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
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
126 changes: 126 additions & 0 deletions examples/contextcaching/ContextCachingAgent.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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();
}
}
111 changes: 111 additions & 0 deletions examples/contextcaching/ContextCachingRun.java
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
54 changes: 54 additions & 0 deletions examples/contextcaching/README.md
Original file line number Diff line number Diff line change
@@ -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="<model name>"
```
Loading
Loading