Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building AI Agents with ADK for Java

Building AI Agents with ADK for Java

Buzzword of 2025, AI agents are becoming mainstream. You don’t even need to use Python to create agents, you can develop them using Java! In this presentation, we’ll focus in particular on one framework: ADK, the Agent Development Kit released by Google.

AI Agents perceive, decide, and act to achieve goals using LLMs and tools. We’ll explore the various tools at our disposal, including built-in ones like Google Search or sandboxed code execution, as well as custom Java code, or MCP servers.

Multi-agent systems can be built by delegating tasks to more specialized sub-agents. We’ll see the various patterns at play to organize agents to work together, using sequential, parallel, or loop flows.

That’s not all, we’ll also look into how callbacks allow you to plug into the AI agent workflow, or how state can be shared and manipulated, and how events flow in our agentic systems or how they are persisted in memory.

At the end of this presentation, you’ll know everything about ADK for Java, and you’ll be able to build your first AI agents in no time!

Avatar for Guillaume Laforge

Guillaume Laforge

October 09, 2025
Tweet

More Decks by Guillaume Laforge

Other Decks in Technology

Transcript

  1. Building AI Agents with ADK (Agent Development Kit) A Deep

    Dive into the Java Framework @glaforge — October 2025
  2. What are AI Agents? An AI Agent is an autonomous

    system that can perceive its environment, make decisions, and take actions to achieve specific goals. • Reasoning — Agents "think" and "plan" using LLMs. • Tool Use — Interact with external systems, APIs, and data sources. • Goal-Oriented — Work to complete tasks defined by their instructions. • Non-Deterministic — Behavior driven by the LLM's reasoning, not a fixed script.
  3. Introducing the Agent Development Kit (ADK) ADK is an Open

    Source framework for building, running, and managing sophisticated AI agents. Available in Python and Java, and soon in other languages. The core component in ADK is the LlmAgent. It's the "brain" of your application, leveraging an LLM for: • Natural Language Understanding • Decision Making • Response Generation • Tool Interaction (“function calling”) google.github.io/adk-docs/
  4. Getting Started — Maven Dependencies Add the following dependencies to

    your pom.xml file. <dependencies> <!-- The core ADK library --> <dependency> <groupId>com.google.adk</groupId> <artifactId>google-adk</artifactId> <version>0.3.0</version> </dependency> <!-- Optional: Dev UI --> <dependency> <groupId>com.google.adk</groupId> <artifactId>google-adk-dev</artifactId> <version>0.3.0</version> </dependency> </dependencies>
  5. Getting Started — Creating Your First Agent First, define the

    agent's identity and purpose using the LlmAgent.builder(). • name: Unique string identifier for the agent. • description: Summary of its capabilities (crucial for multi-agent routing). • model: Underlying LLM to power the agent (e.g., "gemini-2.5-flash"). LlmAgent capitalAgent = LlmAgent.builder() .model("gemini-2.5-flash") .name("capital_agent") .description(""" Answers user questions about the capital city of a given country. """).build();
  6. Guiding the Agent — Instructions The instruction shapes an agent's

    behavior. It tells the agent its goal, personality, constraints, and how to use its tools. LlmAgent capitalAgent = LlmAgent.builder() .model("gemini-2.5-flash") .name("capital_agent") .instruction(""" You are an agent that provides the capital city of a country. When a user asks for the capital of a country: 1. Identify the country name from the user's query. 2. Use the `getCapitalCity` tool to find the capital. 3. Respond clearly to the user, stating the capital city. """) .build();
  7. Play with your agent in the Dev UI Run the

    Dev UI via code: Run the Dev UI via the Maven plugin: AdkWebServer.start(LlmAgent.builder() .name("AI") .model("gemini-2.5-pro") .instruction("You’re an expert!") .build()); $ mvn google-adk:web -Dagents=com.example.MyAgentLoader.INSTANCE
  8. Tools give your agent capabilities beyond the LLM's built-in knowledge.

    They allow it to interact with the outside world, from running code to searching the web or delegating to other agents. ADK offers several types of tools: • Function Tools: For calling your own Java code. • Long-Running Function Tools: For asynchronous tasks. • Built-in Tools: Pre-packaged tools for common tasks (like Google Search, code execution). • Agent as a Tool: For creating multi-agent systems. • MCP Tools: For calling local (STDIO) or remote (SSE/Stream) MCP servers Tools — Equipping the Agent: Tools Overview
  9. Tools — Custom Code with FunctionTool Most common way to

    create a tool is by wrapping a Java method. 1. Define the Java Method: The method must be public (can be static) and return a Map. Use the @Annotations.Schema to describe the parameters to the LLM. @Schema(description = "Retrieve the capital city of a given country") public static Map getCapitalCity( @Schema(name = "country", description = "The country to get capital for") String country) { var capitals = Map.of("france", "Paris", "japan", "Tokyo"); String result = capitals.getOrDefault(country.toLowerCase(), "Not found"); return Map.of("result", result); }
  10. Tools — Custom Code with FunctionTool 2. Create and add

    the FunctionTool to the agent: FunctionTool capitalTool = FunctionTool.create(MyClass.class, "getCapitalCity"); LlmAgent capitalAgent = LlmAgent.builder() /* ... other params ... */ .tools(capitalTool) .build();
  11. Tools — Built-in Tools • Google Search: Allows the agent

    to search the web for up-to-date information. • Python Code Executor: Gives the agent the ability to write and execute Python code to solve complex problems. LlmAgent agent = LlmAgent.builder() /* ... other params ... */ .tools(new GoogleSearchTool()) .build(); LlmAgent agent = LlmAgent.builder() /* ... other params ... */ .tools(new BuiltInCodeExecutionTool()) .build();
  12. Tools — An Agent as a Tool A primary agent

    can delegate specialized tasks to other, more focused agents. This is the foundation of multi-agent workflows. // A specialized agent for financial calculations LlmAgent financeAgent = LlmAgent.builder().name("finance_agent").build(); // A general agent that can use the finance agent as a tool LlmAgent generalAgent = LlmAgent.builder() .name("general_agent") .tools(AgentTool.create(financeAgent)) .build();
  13. Tools — Long-Running & Asynchronous Tools For tasks that take

    a long time to complete, use a LongRunningFunctionTool. Examples: • Waiting for human input (HitL). • Running a lengthy process. This tool acknowledges the request immediately and reports its completion later, preventing the agent from being blocked.
  14. Tools — Calling an MCP Server The Model Context Protocol

    standardizes how to access tools, APIs, and services. Local STDIO, remote HTTP Server Sent Events and Streamable HTTP. SseServerParameters sseParams = SseServerParameters.builder() .url("https://.../mcp/sse") .build(); try (McpToolset mcpToolset = new McpToolset(sseParams)) { List<BaseTool> moonPhasesTools = mcpToolset.getTools(null).toList().blockingGet(); LlmAgent agent = LlmAgent.builder() // ... .tools(moonPhasesTools) .build();
  15. Advanced — Multi-Agent Systems For complex tasks, decompose the problem

    into smaller, specialized agents that work together. ADK provides building blocks to create sophisticated multi-agent workflows. A "parent" agent can have one or more "child" agents (sub-agents). The parent acts as a controller, delegating tasks to its children. Think of ADK as a hierarchical fleet of agents!
  16. Sub-Agents A sub-agent is simply another agent (e.g., LlmAgent) added

    to a parent agent's subAgents list. The parent agent can then transfer control to a sub-agent to handle a specific part of the task. The description of the sub-agent is crucial for the parent LLM to decide which child is best suited for the job.
  17. Sub-Agents LlmAgent researchAgent = LlmAgent.builder() .name("research_agent") .description("Finds information on a

    given topic.") .build(); LlmAgent writerAgent = LlmAgent.builder() .name("writer_agent") .description("Writes a summary based on provided text.") .build(); LlmAgent orchestrator = LlmAgent.builder() .name("orchestrator") .instruction("First, research the topic, then write a summary.") .subAgents(researchAgent, writerAgent) // Add children .build();
  18. Workflow Agents — Sequential Execution A SequentialAgent executes a list

    of sub-agents in a fixed order. It's a deterministic workflow. • The output of one agent is passed as the input to the next. • If any agent in the sequence fails, the entire workflow stops. SequentialAgent sequentialWorkflow = SequentialAgent.builder() .name("blog_post_workflow") .subAgents( // These will run in order new TopicGeneratorAgent(), new ContentResearcherAgent(), new FinalDraftWriterAgent() ) .build();
  19. Workflow Agents — Parallel Execution A ParallelAgent executes all its

    sub-agents concurrently. • Useful for tasks that can be performed independently, like fetching data from multiple sources at once. • The ParallelAgent waits for all sub-agents to complete. • The results are aggregated into a map, with each agent's name as the key. ParallelAgent dataFetcher = ParallelAgent.builder() .name("parallel_data_fetcher") .subAgents( new WeatherApiAgent(), new StockPriceApiAgent(), new NewsApiAgent() ) .build();
  20. Workflow Agents — Loop Execution A LoopAgent repeatedly executes its

    sub-agents until a condition is met. • maxIterations: The maximum number of iterations (defaults to 1). Set to -1 for an infinite loop (requires an exit tool). • You must define a tool to be called by the agent to exit the loop (exit_loop_tool), when the right conditions are met, a callback, or a code-based agent. They must call context.eventActions().setEscalate(true) to exit. LoopAgent dataFetcher = LoopAgent.builder() .name("draft_and_improve") .subAgents( new DraftWriterAgent(), new EvaluatorAgent(), new RefinerAgent() ) .build();
  21. Execution Flow & the Runner The Runner is responsible for

    executing the agent's logic. It manages the interaction loop between the user, the agent (LLM), and the tools. • It takes a user message. • Sends it to the LlmAgent. • The agent decides whether to respond, call a tool, or transfer to another agent. • The Runner executes the tool call and sends the result back to the agent. • This continues until the agent produces a final response. You interact with the Runner via its runAsync method, which returns a stream of Event objects.
  22. Session Management ADK needs to manage conversation history and state.

    This is handled by a SessionService. • InMemorySessionService: A simple, in-memory implementation perfect for getting started. It stores conversation history and state for each session. • Session State: A key-value map (session.state()) where you can store data that needs to persist across turns in a conversation.
  23. // 1. Create a session service InMemorySessionService sessionService = new

    InMemorySessionService(); // 2. Create a session for a user sessionService.createSession(APP_NAME, USER_ID, null, SESSION_ID).blockingGet(); // 3. Initialize the Runner with the service Runner capitalRunner = new Runner(capitalAgent, APP_NAME, … sessionService, …); // 4. Run the agent Flowable<Event> eventStream = capitalRunner.runAsync(USER_ID, SESSION_ID, userContent); Event Loop & Session Management
  24. Using Artifacts and State in Instructions Make your instructions dynamic

    by referencing session state and artifacts. • State Variables: Use {variableName} in your instruction to insert a value from the session state map. ◦ Example: session.state().put("name", "Alex"); ◦ Instruction: "Your name is {name}." → "Your name is Alex." • Artifacts: Artifacts are files or data associated with the session. Use {artifact.artifactName} to insert the text content of an artifact into the prompt. This is useful for providing large documents or data files as context to the agent.
  25. For tasks requiring structured data, define an inputSchema and outputSchema.

    • inputSchema: Enforces that the input to the agent is a JSON string conforming to a specific Schema. • outputSchema: Forces the agent's final response to be a JSON string conforming to the schema. 🚸 Constraint: Using outputSchema disables tool use The LLM must generate the JSON directly. Advanced — Structured I/O with Schemas
  26. // Define an output schema Schema CAPITAL_INFO_OUTPUT_SCHEMA = Schema.builder() .type("OBJECT")

    .properties(Map.of("capital", Schema.builder().type("STRING").build())) .build(); // Build the agent LlmAgent structuredAgent = LlmAgent.builder() .instruction("Respond ONLY with a JSON object containing the capital.") .outputSchema(CAPITAL_INFO_OUTPUT_SCHEMA) .outputKey("structured_info_result") // Save result to session state .build(); Advanced — Structured I/O with Schemas
  27. Advanced — Intercepting with Callbacks Callbacks allow you to hook

    into the agent's execution lifecycle to: • add logging, • modify data, or • trigger external processes… Powerful mechanism for observing and controlling the agent's behavior without altering its core logic. A callback can return Maybe.empty() to continue the normal execution, or it can return a value to override the default behavior.
  28. Agent Lifecycle Callbacks Agent callbacks are triggered at the beginning

    and end of an agent's execution. • beforeAgentCallback(CallbackContext context) • afterAgentCallback(CallbackContext context) LlmAgent agent = LlmAgent.builder() .name("lifecycle-agent") .beforeAgentCallback(context -> { System.out.println("Agent " + context.agentName() + " starting"); return Maybe.empty(); }) .build();
  29. Callbacks — LLM Interaction Model callbacks are specific to LlmAgent

    and are invoked before and after an interaction with the LLM. • beforeModelCallback(CallbackContext ctx, LlmRequest req) • afterModelCallback(CallbackContext ctx, LlmResponse resp) LlmAgent agent = LlmAgent.builder() .name("llm-interaction-agent") .afterModelCallback((context, response) -> { System.out.println("LLM response received."); return Maybe.empty(); }).build();
  30. Callbacks — Tool Execution Tool callback are specific to LlmAgent

    and are triggered before and after the execution of a tool. • beforeToolCallback(ToolContext ctx) • afterToolCallback(ToolContext ctx, Map<String, ?> resp) LlmAgent agent = LlmAgent.builder().name("tool-execution-agent") .tools(myTool) .beforeToolCallback(context -> { System.out.println("Executing tool: " + context.toolName()); return Maybe.empty(); }).build();
  31. Configure all models supported by LangChain4j Thanks to the google-adk-langchain4j

    module. Allows you to configure any chat model supported by LangChain4j OllamaChatModel ollamaChatModel = OllamaChatModel.builder() .modelName("qwen3:1.7b") .baseUrl("http://127.0.0.1:11434") .build(); LlmAgent scienceTeacherAgent = LlmAgent.builder() .name("science-app") .description("Science teacher agent") .model(new LangChain4j(ollamaChatModel)) .instruction(""" You are a helpful science teacher who explains science concepts to kids and teenagers. """).build();
  32. Declarative YAML agent definition Describe agents via a YAML agent

    definition file, and load them with the ConfigAgentUtils utility. name: root_agent instruction: | You delegate coding questions to the code_tutor_agent and math questions to the math_tutor_agent. sub_agents: - config_path: ./code_tutor_agent.yaml # Ref. another agent - code: com.example.MySpecialAgent.INSTANCE # Ref. a Java agent tools: - name: google_search - name: com.example.MyCustomTools.WEATHER_TOOL
  33. Memory service for long term memory Provide long term memory

    service to agents to remember past conversations // When building your agent LlmAgent agent = LlmAgent.builder() .name("memory_agent") .tools(new LoadMemoryTool()) .instruction( "You are a helpful assistant with a long-term memory.") .build();
  34. Provide long term memory service to agents to remember past

    conversations // During application setup BaseMemoryService memoryService = new InMemoryMemoryService(); Runner runner = new Runner( agent, appName, artifactService, sessionService, memoryService); Memory service for long term memory // After a session concludes (or via a callback) Session completedSession = sessionService.getSession(...).blockingGet(); memoryService.addSessionToMemory(completedSession).blockingAwait();
  35. Plugin system for extensibility Register & configure plugins to extend

    the agent lifecycle, with callbacks. • Define your plugin by extending BasePlugin public class LoggingPlugin extends BasePlugin { private static final Logger logger = ... public LoggingPlugin() { super("SimpleLogger"); } public Maybe<Content> beforeAgentCallback( BaseAgent agent, CallbackContext context) { logger.info("Agent starting: {}", agent.name()); return Maybe.empty(); // Continue execution } }
  36. Register & configure plugins to extend the agent lifecycle, with

    the usual LlmAgent callbacks, plus before/afterRunCallback(), onUserMessageCallback(), onEventCallback(), onModelErrorCallback(), and onToolErrorCallback(). • Register the plugin with the PluginManager: • Passe the plugin manager to the Runner: PluginManager pluginManager = new PluginManager(List.of(new LoggingPlugin())); Plugin system for extensibility Runner runner = new Runner(agent, appName, ..., pluginManager);
  37. Advanced code execution capabilities For solving complex problems, agents can

    generate code and execute code. • Built-in code-execution (since Gemini 1.5): LlmAgent agent = LlmAgent.builder() .model("gemini-2.5-pro") .tools(new BuiltInCodeExecutionTool()) .build();
  38. Advanced code execution capabilities For solving complex problems, agents can

    generate code and execute code. • Code execution with the ContainerCodeExecutor: ContainerCodeExecutor dockerExecutor = new ContainerCodeExecutor( Optional.empty(), // base URL, use default Docker daemon Optional.of("my-python-env:latest"), // image Optional.of("./path/to/docker_dir") // docker path ); LlmAgent agent = LlmAgent.builder() .name("docker_code_agent") .codeExecutor(dockerExecutor) .build();
  39. Advanced code execution capabilities For solving complex problems, agents can

    generate code and execute code. • Code execution with the VertexAiCodeExecutor: VertexAiCodeExecutor vertexExecutor = new VertexAiCodeExecutor( "projects/my-gcp-project/locations/us-central1/extensions/my-ext-id" ); LlmAgent agent = LlmAgent.builder() .name("vertex_code_agent") .codeExecutor(vertexExecutor) .build();
  40. Starting agents with a (J)Bang! Easily define simple AI agents

    in a Java source file, with the new class & main method syntax. Define dependencies as JBang DEPS comments. From the terminal, start with: $ jbang AI.java //JAVA 17+ //PREVIEW //DEPS com.google.adk:google-adk-dev:0.3.0 import com.google.adk.agents.LlmAgent; import com.google.adk.web.AdkWebServer; void main() { AdkWebServer.start(LlmAgent.builder() .name("AI") .model("gemini-2.5-pro") .instruction("Be very grumpy!") .build()); }
  41. Summary ADK for Java provides a powerful and flexible framework

    for building AI agents. • LlmAgent is the core reasoning engine. • Instructions are key to guiding agent behavior. • Schemas enable reliable, structured data exchange. • Tools extend agent capabilities to interact with the world. • SequentialAgent, ParallelAgent, LoopAgent, and sub-agents allow you to make your agents behavior more deterministic. • Runner and SessionService manage the execution flow and state. • Callbacks give you access to key points in the lifecycle of your agent behavior. • Can create agents declaratively via YAML definitions • Supports reusing agent plumbing logic via plugins