From 91a183865e5e14499e827961cb96df24a0476ea1 Mon Sep 17 00:00:00 2001 From: Kamil Tomaszek Date: Tue, 13 Oct 2026 03:03:13 -0700 Subject: [PATCH] feat: add a context caching demo sample PiperOrigin-RevId: 998505125 --- examples/README.md | 2 + .../contextcaching/ContextCachingAgent.java | 126 ++++++++++++++++++ .../contextcaching/ContextCachingRun.java | 111 +++++++++++++++ examples/contextcaching/README.md | 54 ++++++++ examples/contextcaching/pom.xml | 112 ++++++++++++++++ examples/pom.xml | 1 + 6 files changed, 406 insertions(+) create mode 100644 examples/contextcaching/ContextCachingAgent.java create mode 100644 examples/contextcaching/ContextCachingRun.java create mode 100644 examples/contextcaching/README.md create mode 100644 examples/contextcaching/pom.xml diff --git a/examples/README.md b/examples/README.md index 7ab11f6b1..65768a1b9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,6 +13,8 @@ contains more complete end-to-end samples for customers to use or modify. - [`a2a_server`](a2a_server/README.md): an ADK agent served over A2A with Quarkus. - [`configagent`](configagent/README.md): agents defined in YAML config files. +- [`contextcaching`](contextcaching/README.md): Gemini context caching that + reuses and refreshes a cache across turns. - [`helloworld`](helloworld/README.md): a minimal agent with dice-rolling and prime-checking tools. - [`mcpfilesystem`](mcpfilesystem/README.md): an agent that uses the MCP diff --git a/examples/contextcaching/ContextCachingAgent.java b/examples/contextcaching/ContextCachingAgent.java new file mode 100644 index 000000000..2ee3bb495 --- /dev/null +++ b/examples/contextcaching/ContextCachingAgent.java @@ -0,0 +1,126 @@ +/* + * 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.example.contextcaching; + +import com.google.adk.agents.ContextCacheConfig; +import com.google.adk.agents.LlmAgent; +import com.google.adk.apps.App; +import com.google.common.collect.ImmutableList; +import java.time.Duration; + +/** A weather assistant whose large, shared instruction makes it a good fit for context caching. */ +public final class ContextCachingAgent { + + static final String DEFAULT_MODEL = "gemini-3.8-flash"; + + /** + * The shortest TTL Vertex AI accepts, so the demo can let the cache expire and create it again. + */ + static final Duration CACHE_TTL = Duration.ofMinutes(1); + + /** + * The system instruction, which is the bulk of the cached prefix. Explicit caching only starts + * once that prefix reaches the model's minimum (2048 tokens for Gemini 2.5, 4096 for Gemini 3), + * so generated station rows pad it past that. + */ + private static final String WEATHER_REFERENCE = buildWeatherReference(); + + private ContextCachingAgent() {} + + /** Returns an app that enables context caching for all its agents. */ + public static App createApp(String model) { + LlmAgent agent = + LlmAgent.builder() + .name("weather_assistant") + .description("Answers weather questions from a large shared reference.") + .model(model) + .instruction(WEATHER_REFERENCE) + .build(); + return App.builder() + .name("context_caching_demo") + .rootAgent(agent) + .contextCacheConfig( + new ContextCacheConfig(/* cacheIntervals= */ 10, CACHE_TTL, /* minTokens= */ 0)) + .build(); + } + + private static String buildWeatherReference() { + StringBuilder reference = new StringBuilder(); + reference.append( + "You are a weather assistant. Answer questions using ONLY the weather reference below," + + " keep answers to one or two sentences, and cite the section or city you used.\n\n"); + + ImmutableList guidelines = + ImmutableList.of( + "Forecasting basics: a forecast combines current observations, numerical model" + + " guidance, and local climatology; always state the valid time window and your" + + " confidence.", + "Wind chill: how cold the air feels once wind is accounted for. It is only defined for" + + " temperatures at or below 10C and wind above 5 km/h.", + "Heat index: the apparent temperature, combining air temperature and humidity. It is" + + " meaningful above about 27C when relative humidity is high.", + "Precipitation types: rain, drizzle, sleet, freezing rain, snow, and hail are" + + " distinguished by the temperature profile between the cloud base and the" + + " ground.", + "Storm safety: during a thunderstorm, move indoors, avoid open fields and tall isolated" + + " trees, and stay off corded electronics until 30 minutes after the last" + + " thunder.", + "Units: temperatures are in degrees Celsius, wind speed in km/h, and precipitation in" + + " millimeters unless a value states otherwise."); + for (int i = 0; i < guidelines.size(); i++) { + reference.append("Section ").append(i + 1).append(": ").append(guidelines.get(i)); + reference.append('\n'); + } + + reference.append("\nClimate normals for named cities:\n"); + ImmutableList cities = + ImmutableList.of( + "Marisol: coastal and mild; January average 12C, July average 24C, annual rainfall" + + " 640 mm.", + "Fjordheim: cold maritime; January average -6C, July average 15C, annual rainfall" + + " 900 mm.", + "Solara: hot desert; January average 18C, July average 41C, annual rainfall 90 mm.", + "Verdant: temperate rainforest; January average 7C, July average 19C, annual rainfall" + + " 2400 mm.", + "Highpoint: alpine; January average -9C, July average 12C, annual rainfall 1100 mm.", + "Puerto Brisa: tropical; January average 26C, July average 29C, annual rainfall" + + " 1800 mm.", + "Windgate: windy plains; January average -2C, July average 27C, annual rainfall" + + " 520 mm.", + "Frosthollow: subarctic; January average -22C, July average 16C, annual rainfall" + + " 380 mm."); + cities.forEach(city -> reference.append(" ").append(city).append('\n')); + + reference.append("\nAutomated station normals:\n"); + ImmutableList windDirections = + ImmutableList.of("N", "NE", "E", "SE", "S", "SW", "W", "NW"); + for (int i = 1; i <= 100; i++) { + reference.append( + String.format( + " Station S%d (sector %c%d): January average %dC, July average %dC, annual rainfall" + + " %d mm, prevailing wind %s at %d km/h.\n", + i, + (char) ('A' + i % 8), + i % 12, + -12 + (i * 7) % 34, + 14 + (i * 3) % 22, + 250 + (i * 37) % 2300, + windDirections.get(i % 8), + 6 + (i * 5) % 38)); + } + return reference.toString(); + } +} diff --git a/examples/contextcaching/ContextCachingRun.java b/examples/contextcaching/ContextCachingRun.java new file mode 100644 index 000000000..494e21f89 --- /dev/null +++ b/examples/contextcaching/ContextCachingRun.java @@ -0,0 +1,111 @@ +/* + * 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.example.contextcaching; + +import com.google.adk.apps.App; +import com.google.adk.events.Event; +import com.google.adk.models.CacheMetadata; +import com.google.adk.runner.Runner; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import java.time.Duration; +import org.jspecify.annotations.Nullable; + +/** + * Runs several turns of one session against {@link ContextCachingAgent} and prints what the cache + * does on each turn. It pauses once so the cache expires, and the next turn creates a new one. + * Exact turn-by-turn output depends on model latency, since a slow turn can outlast the TTL. + */ +public final class ContextCachingRun { + + private static final ImmutableList PROMPTS = + ImmutableList.of( + "In one sentence, what should I do during a thunderstorm?", + "How is wind chill defined, and when does it apply?", + "What units does the reference use for wind speed?", + "What is the average annual rainfall listed for Verdant?", + "Which named city is the driest?", + "What is the July average temperature for Puerto Brisa?"); + + /** Zero-based turn after which the demo waits for the cache to expire. */ + private static final int EXPIRY_AFTER_TURN = 2; + + private static final String USER_ID = "demo-user"; + + private ContextCachingRun() {} + + public static void main(String[] args) throws InterruptedException { + String model = args.length > 0 ? args[0] : ContextCachingAgent.DEFAULT_MODEL; + App app = ContextCachingAgent.createApp(model); + Runner runner = Runner.builder().app(app).build(); + String sessionId = + runner.sessionService().createSession(app.name(), USER_ID).blockingGet().id(); + + System.out.printf( + "Context caching demo on %s, cache TTL %ds.%n", + model, ContextCachingAgent.CACHE_TTL.toSeconds()); + + String activeCacheName = null; + for (int turn = 0; turn < PROMPTS.size(); turn++) { + String prompt = PROMPTS.get(turn); + System.out.printf("%n===== Turn %d: %s =====%n", turn + 1, prompt); + Event answer = + runner + .runAsync(USER_ID, sessionId, Content.fromParts(Part.fromText(prompt))) + .blockingLast(); + System.out.println("assistant> " + answer.stringifyContent().strip()); + + CacheMetadata cache = answer.cacheMetadata().orElse(null); + if (cache != null) { + System.out.printf( + " cache: %s | cachedContents=%d invocationsUsed=%d%n", + describe(cache, activeCacheName), + cache.contentsCount(), + cache.invocationsUsed().orElse(0)); + activeCacheName = cache.cacheName().orElse(activeCacheName); + } + answer + .usageMetadata() + .ifPresent( + usage -> + System.out.printf( + " prompt tokens: %d, served from cache: %d%n", + usage.promptTokenCount().orElse(0), + usage.cachedContentTokenCount().orElse(0))); + + if (turn == EXPIRY_AFTER_TURN) { + Duration wait = ContextCachingAgent.CACHE_TTL.plusSeconds(2); + System.out.printf("%n... waiting %ds so the cache expires ...%n", wait.toSeconds()); + Thread.sleep(wait.toMillis()); + } + } + } + + /** Describes a turn's cache relative to the cache the previous turns used. */ + private static String describe(CacheMetadata cache, @Nullable String previousCacheName) { + String name = cache.cacheName().orElse(null); + if (name == null) { + return previousCacheName == null + ? "no cache yet (fingerprint only)" + : "no active cache (fingerprint only)"; + } + if (previousCacheName == null) { + return "CREATED " + name; + } + return name.equals(previousCacheName) ? "REUSED " + name : "RE-CREATED " + name; + } +} diff --git a/examples/contextcaching/README.md b/examples/contextcaching/README.md new file mode 100644 index 000000000..147534be7 --- /dev/null +++ b/examples/contextcaching/README.md @@ -0,0 +1,54 @@ +# Context Caching Sample + +This sample shows explicit context caching with the Google ADK. A weather +assistant answers several questions in one session. Its large system instruction +is shared by every turn, so the app caches it on the Gemini backend with +`App.contextCacheConfig`, and later turns reference the cached instruction +instead of resending it. + +The runner prints what the cache does on each turn: + +- Turn 1 has no cache yet. It only records a fingerprint of the cacheable + prefix. +- Once the prefix is known to be stable, a cache is CREATED. Later turns REUSE + it, and `invocationsUsed` grows. +- After the third turn the runner waits for the cache's one-minute TTL, the + shortest Vertex AI accepts, to run out. The next turn RE-CREATES the cache, + usually with more cached contents. +- Each turn also prints its prompt token count and how many of those tokens + the cache served. + +Exact turn-by-turn output depends on model latency. A slow turn can outlast the +one-minute TTL, so a later turn may print an extra RE-CREATED. + +Caching only starts once the cached prefix reaches the model's minimum size: +2048 tokens for Gemini 2.5 and 4096 tokens for Gemini 3. + +## Project Layout + +``` +├── ContextCachingAgent.java // Agent, app and caching configuration +├── ContextCachingRun.java // Console runner entry point +├── pom.xml // Maven configuration and exec main class +└── README.md // This file +``` + +## Prerequisites + +- Java 17+ +- Maven 3.9+ +- Credentials for the Gemini API (`GOOGLE_API_KEY`) or Vertex AI + (`GOOGLE_GENAI_USE_VERTEXAI=true`, `GOOGLE_CLOUD_PROJECT` and + `GOOGLE_CLOUD_LOCATION`) + +## Build and Run + +```bash +mvn clean compile exec:java +``` + +The sample uses `gemini-3.8-flash`. To try another model, pass its name: + +```bash +mvn compile exec:java -Dexec.args="" +``` diff --git a/examples/contextcaching/pom.xml b/examples/contextcaching/pom.xml new file mode 100644 index 000000000..70dbf49c3 --- /dev/null +++ b/examples/contextcaching/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + + com.google.adk + google-adk-examples + 1.11.1-SNAPSHOT + .. + + + com.google.adk.samples + google-adk-sample-contextcaching + Google ADK - Sample - Context Caching + + A sample application demonstrating context caching with the Google ADK, + runnable via com.example.contextcaching.ContextCachingRun. + + jar + + + UTF-8 + 17 + + com.example.contextcaching.ContextCachingRun + ${project.version} + + + + + com.google.adk + google-adk + ${google-adk.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + true + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-source + generate-sources + + add-source + + + + . + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + **/*.jar + target/** + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.codehaus.mojo + exec-maven-plugin + 3.2.0 + + ${exec.mainClass} + runtime + + + + + diff --git a/examples/pom.xml b/examples/pom.xml index cfb1517a0..eb3fd8a9d 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -34,6 +34,7 @@ a2a_basic a2a_server configagent + contextcaching helloworld mcpfilesystem