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
31 changes: 28 additions & 3 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,8 @@
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.web.config.BackendUrl;
import com.google.adk.web.config.DevUiAssets;
import com.google.common.annotations.VisibleForTesting;
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 +117,9 @@
* @return A configured MappingJackson2HttpMessageConverter.
*/
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(

Check warning on line 120 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 120 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 120 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 120 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 120 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 120 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 120 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 120 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 120 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 120 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 120 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 120 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 122 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 122 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 122 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 +175,28 @@

// TODO: return a Closeable that stops the server and resets the static state.
public static void start(BaseAgent... agents) {
startWithLoader(new String[0], "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) {
startApps(new String[0], app, moreApps);
}

/** Like {@link #start(App, App...)}, passing {@code args} to Spring and returning its context. */
@VisibleForTesting
static ConfigurableApplicationContext startApps(String[] args, App app, App... moreApps) {
return startWithLoader(
args, "appLoader", new InMemoryAppLoader(ObjectArrays.concat(app, moreApps)));
}

private static ConfigurableApplicationContext startWithLoader(
String[] args, 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,13 +209,13 @@
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);
}
});

app.run(new String[0]);
return app.run(args);
}
}
46 changes: 46 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,46 @@
/*
* 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 serves only that loader's apps and ignores any
* {@link CompiledAgentLoader}, though the default one is still created and scans {@code
* adk.agents.source-dir} unless {@code adk.agents.loader} names another loader; any other {@link
* AgentLoader} fails startup.
*/
@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 IllegalStateException 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

import static java.util.stream.Collectors.toList;

import com.google.adk.artifacts.InMemoryArtifactService;
import com.google.adk.memory.InMemoryMemoryService;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.web.AgentLoader;
import com.google.adk.web.service.RunnerService;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.slf4j.Logger;
Expand All @@ -33,34 +37,52 @@ public class AgentController {

private static final Logger log = LoggerFactory.getLogger(AgentController.class);

private final AgentLoader agentProvider;
private final RunnerService runnerService;

/**
* Constructs the AgentController.
*
* @param agentProvider The provider for loading agents.
* @param runnerService The service serving the configured apps.
*/
@Autowired
public AgentController(AgentLoader agentProvider) {
this.agentProvider = agentProvider;
ImmutableList<String> agentNames = this.agentProvider.listAgents();
public AgentController(RunnerService runnerService) {
this.runnerService = runnerService;
ImmutableList<String> agentNames = runnerService.listApps();
log.info(
"AgentController initialized with {} dynamic agents: {}", agentNames.size(), agentNames);
if (agentNames.isEmpty()) {
log.warn(
"Agent registry is empty. Check 'adk.agents.source-dir' property and compilation"
+ " logs.");
"No apps are served. Check the configured loader, such as the 'adk.agents.source-dir'"
+ " property and compilation logs for compiled agents.");
}
}

/**
* Lists available applications. Currently returns only the configured root agent's name.
* Constructs an AgentController that serves only the agents of {@code agentProvider}.
*
* @param agentProvider The provider for loading agents.
* @deprecated Use {@link #AgentController(RunnerService)} instead.
*/
@Deprecated
public AgentController(AgentLoader agentProvider) {
// Listing apps reads only the loader, so these runner services are never used.
this(
new RunnerService(
agentProvider,
new InMemoryArtifactService(),
new InMemorySessionService(),
new InMemoryMemoryService(),
ImmutableList.of()));
}

/**
* Lists the available applications.
*
* @return A list containing the root agent's name.
* @return The names of the served apps, sorted.
*/
@GetMapping("/list-apps")
public List<String> listApps() {
ImmutableList<String> agentNames = agentProvider.listAgents();
ImmutableList<String> agentNames = runnerService.listApps();
log.info("Listing apps from dynamic registry. Found: {}", agentNames);
return agentNames.stream().sorted().collect(toList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package com.google.adk.web.controller;

import com.google.adk.agents.BaseAgent;
import com.google.adk.artifacts.InMemoryArtifactService;
import com.google.adk.events.Event;
import com.google.adk.memory.InMemoryMemoryService;
import com.google.adk.sessions.BaseSessionService;
import com.google.adk.sessions.Session;
import com.google.adk.web.AgentGraphGenerator;
import com.google.adk.web.AgentLoader;
import com.google.adk.web.dto.GraphResponse;
import com.google.adk.web.service.RunnerService;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionResponse;
Expand All @@ -48,12 +51,32 @@ public class GraphController {
private static final Logger log = LoggerFactory.getLogger(GraphController.class);

private final BaseSessionService sessionService;
private final AgentLoader agentProvider;
private final RunnerService runnerService;

@Autowired
public GraphController(BaseSessionService sessionService, AgentLoader agentProvider) {
public GraphController(BaseSessionService sessionService, RunnerService runnerService) {
this.sessionService = sessionService;
this.agentProvider = agentProvider;
this.runnerService = runnerService;
}

/**
* Constructs a GraphController that draws only the agents of {@code agentProvider}.
*
* @param sessionService The service holding the sessions whose events are drawn.
* @param agentProvider The provider for loading agents.
* @deprecated Use {@link #GraphController(BaseSessionService, RunnerService)} instead.
*/
@Deprecated
public GraphController(BaseSessionService sessionService, AgentLoader agentProvider) {
// Loading an app's root reads only the loader, so these runner services are never used.
this(
sessionService,
new RunnerService(
agentProvider,
new InMemoryArtifactService(),
sessionService,
new InMemoryMemoryService(),
ImmutableList.of()));
}

/**
Expand Down Expand Up @@ -124,18 +147,25 @@ public ResponseEntity<GraphResponse> getEventGraph(
sessionId,
eventId);

BaseAgent currentAppAgent;
Optional<BaseAgent> rootAgent;
try {
currentAppAgent = agentProvider.loadAgent(appName);
} catch (java.util.NoSuchElementException e) {
log.warn("Agent app '{}' not found for graph generation.", appName);
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new GraphResponse("Agent app not found: " + appName));
} catch (IllegalStateException e) {
log.warn("Agent app '{}' failed to load for graph generation: {}", appName, e.getMessage());
// This route draws agent trees only, so an app that runs a workflow node has no graph here.
rootAgent =
runnerService
.loadRoot(appName)
.filter(BaseAgent.class::isInstance)
.map(BaseAgent.class::cast);
} catch (RuntimeException e) {
log.warn("The requested agent app failed to load for graph generation.", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new GraphResponse("Agent app failed to load: " + appName));
.body(new GraphResponse("Agent app not loaded"));
}
if (rootAgent.isEmpty()) {
log.warn("No agent app is served under the requested name for graph generation.");
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new GraphResponse("Agent app not found"));
}
BaseAgent currentAppAgent = rootAgent.get();

Session session = findSessionOrThrow(appName, userId, sessionId);
Event event =
Expand Down
Loading
Loading