Grounding with Google Search lets a Gemini model look things up before it answers. It then returns the queries it ran, the pages it used, and which sentences each page supports. In ADK for Java this is a single line: add GoogleSearchTool.INSTANCE to an LlmAgent. Production use needs more. You need citations that land on the right characters, search that still works when the agent also has your own tools, a way to control cost, and an answer to what happens when search returns nothing useful.

This article covers what the tool sends to the model, what comes back, how to render citations without off-by-one errors, why the usual workaround for combining search with other tools quietly drops the citations, and how to fix that. Every class and method named here was checked against the adk-java v1.11.0 release (2 October 2026) and the google-genai Java types. Model limits change often, so where a limit depends on the model generation, the article says so instead of quoting a number.

What GoogleSearchTool actually does

GoogleSearchTool runs no Java code: it edits the request, and Gemini does the searchingRunneruser turnLlmAgent flowpreprocess requestprocessLlmRequestadds Tool.googleSearchGeminiplans queriesGoogle Searchresults to modelLlmResponsetext parts + GroundingMetadataEvent.groundingMetadata()your code renders citationsFields: webSearchQueries, groundingChunks (web uri, title),groundingSupports (byte segments, chunk indices), searchEntryPointWrapped in AgentTool?only the final text survives
Request and response path for GoogleSearchTool. The search happens inside the model call, so it never appears in your tool traces.

Read the source and the tool turns out to be small. GoogleSearchTool is a final class with the name google_search. It has no runAsync implementation and no function declaration. Its only behaviour is in processLlmRequest, which copies the request's GenerateContentConfig, appends Tool.builder().googleSearch(GoogleSearch.builder().build()), and puts the config back. In other words, the request is sent with search enabled and no options set. The model decides whether to search, what to search for, and how many queries to run.

This has three practical consequences. First, there is nothing to configure. The class has no builder, project or location parameters, and any snippet that shows them does not compile. Credentials and backend (Gemini API or Vertex AI) come from the model client. Second, the search never appears as a tool call, so ADK's before-tool and after-tool callbacks do not see it, and neither does a span per query. Third, the evidence arrives as metadata on the model response, not as a tool result. ADK copies it to LlmResponse.groundingMetadata() and then to Event.groundingMetadata().

LlmAgent researcher = LlmAgent.builder()
    .name("researcher")
    .model("gemini-2.5-flash")
    .instruction("Answer questions about public facts. Search when the answer may have "
        + "changed recently or you are not certain. Say so when sources disagree.")
    .tools(GoogleSearchTool.INSTANCE)
    .build();

InMemoryRunner runner = new InMemoryRunner(researcher);
// ... create a session, then for each event of runner.runAsync(userId, sessionId, message):
event.groundingMetadata().ifPresent(gm ->
    log.info("queries={} sources={}",
        gm.webSearchQueries().orElse(List.of()),
        gm.groundingChunks().map(List::size).orElse(0)));

What comes back

GroundingMetadata fieldWhat it holdsUse it for
webSearchQueries()The queries the model actually ranCost accounting, debugging bad answers
groundingChunks()Sources; each web() has uri() and title()The numbered source list
groundingSupports()Answer segments, each with groundingChunkIndices() and confidenceScores()Inline citation markers
searchEntryPoint()renderedContent(): an HTML snippet of search suggestionsDisplay in your UI; read the grounding terms
web().domain()Source domainVertex AI only; marked unsupported on the Gemini API

Treat every field as optional. A turn where the model chose not to search has no metadata. A turn with metadata can still have supports that cover only part of the answer, and unsupported sentences are exactly the ones a reviewer should look at first. The Gemini API documentation says the full usage requirements for search suggestions are in the Terms of Service. Read them before you design a UI that hides the suggestions.

Citations are byte offsets

Each support has a Segment with startIndex() and endIndex(). The SDK documentation is explicit that these are measured in bytes within one Part. Java strings are indexed in UTF-16 code units. For ASCII text the two agree, so code that calls text.substring(start, end) passes every English test. It breaks on the first accented name.

Take the answer Zürich raised its rate. Geneva did not. It is 39 characters long and 40 bytes long in UTF-8, because ü takes two bytes. The first sentence ends at byte 24 but at character 23. Inserting a marker at index 24 puts it after the following space. The second sentence ends at byte 40. As a character index, 40 is past the end of the string, so substring throws StringIndexOutOfBoundsException. Do all the work on the byte array instead:

static String withCitations(String text, GroundingMetadata gm) {
  byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);
  int sources = gm.groundingChunks().map(List::size).orElse(0);
  TreeMap<Integer, TreeSet<Integer>> marks = new TreeMap<>();  // byte offset -> source numbers
  for (GroundingSupport s : gm.groundingSupports().orElse(List.of())) {
    int end = s.segment().flatMap(Segment::endIndex).orElse(-1);
    boolean onBoundary = end == utf8.length || (end >= 0 && end < utf8.length && (utf8[end] & 0xC0) != 0x80);
    if (!onBoundary) continue;                                  // stale or mid-character: skip
    for (int i : s.groundingChunkIndices().orElse(List.of())) {
      if (i < sources) marks.computeIfAbsent(end, k -> new TreeSet<>()).add(i + 1);
    }
  }
  StringBuilder out = new StringBuilder();
  int from = 0;
  for (Map.Entry<Integer, TreeSet<Integer>> e : marks.entrySet()) {
    out.append(new String(utf8, from, e.getKey() - from, StandardCharsets.UTF_8));
    e.getValue().forEach(n -> out.append('[').append(n).append(']'));
    from = e.getKey();
  }
  return out.append(new String(utf8, from, utf8.length - from, StandardCharsets.UTF_8)).toString();
}

This assumes the answer is a single text part. If a response has several parts, group the supports by segment().partIndex() and apply the function to each part separately. Never apply offsets to text you have already edited, such as text with markdown stripped. Compute the markers on the exact text the model returned, then transform that.

Search next to other tools without losing sources

The ADK documentation does not exempt Java from the one-built-in rule: an agent that uses Google Search cannot use any other tool, and putting a built-in tool on a sub-agent is not supported. Newer Gemini models accept built-in tools mixed with function calling at the API level, and ADK Java's GoogleSearchTool does not check either way. If you mix them anyway, you are relying on behaviour the framework does not document, so test it on your exact model. The supported pattern is to put search in its own agent and call that agent as a tool. ADK even ships GoogleSearchAgentTool.create(BaseLlm model), whose source describes it as a workaround for exactly this.

Read AgentTool.runAsync before relying on it. It runs the wrapped agent in a nested runner and keeps the last event's text. It returns {"result": text}, or the parsed output schema if the agent has one. Grounding metadata is attached to the inner event and is never copied out. The parent agent receives a fluent paragraph with no sources, and from that point on nothing can show which claim came from where. The fix is to put the evidence into the text before the wrapper drops the metadata. ADK replaces a model response with whatever an after-model callback returns, so the search agent can rewrite its own answer:

LlmAgent searcher = LlmAgent.builder()
    .name("web_searcher")
    .model("gemini-2.5-flash")
    .description("Searches the public web; returns an answer with [n] markers and sources.")
    .instruction("Answer the request using google_search. Be brief and factual.")
    .tools(GoogleSearchTool.INSTANCE)
    .afterModelCallback((ctx, resp) -> {
        Optional<String> text = resp.content().map(Content::text);
        if (text.isEmpty() || resp.groundingMetadata().isEmpty()) return Maybe.empty();
        GroundingMetadata gm = resp.groundingMetadata().get();
        String cited = withCitations(text.get(), gm) + "\n\nSources:\n" + sourceList(gm);
        return Maybe.just(resp.toBuilder()
            .content(Content.fromParts(Part.fromText(cited)).toBuilder().role("model").build())
            .build());
    })
    .build();

LlmAgent support = LlmAgent.builder()
    .name("support_agent")
    .model("gemini-2.5-flash")
    .instruction("Use web_searcher for public facts and order_lookup for orders. "
        + "When you use web_searcher, keep its [n] markers and copy its source list.")
    .tools(AgentTool.create(searcher), orderLookupTool)
    .build();

sourceList numbers the chunks' titles and URIs in the same order as the markers. The parent model can still drop markers when it paraphrases, so check in an after-agent step that every marker in the final answer points at a listed source. The pattern is described further in grounded answers with citations in ADK Java.

Configured search: options and backends

Because GoogleSearchTool is final and sends an empty GoogleSearch, any search options require your own BaseTool that does the same thing with a populated object. The google-genai types define three options, and the backend decides which of them you can use. excludeDomains and blockingConfidence are documented as unsupported on the Gemini API, which means Vertex AI only. timeRangeFilter (an Interval with start and end time) is documented as unsupported on Vertex AI. These fields are recent, so check the genai version your ADK release actually resolves.

public final class ConfiguredGoogleSearchTool extends BaseTool {
  private final GoogleSearch search;

  public ConfiguredGoogleSearchTool(GoogleSearch search) {
    super("google_search", "google_search");
    this.search = search;
  }

  @Override
  public Completable processLlmRequest(LlmRequest.Builder request, ToolContext ctx) {
    GenerateContentConfig config =
        request.build().config().orElseGet(() -> GenerateContentConfig.builder().build());
    List<Tool> tools = new ArrayList<>(config.tools().orElse(List.of()));
    tools.add(Tool.builder().googleSearch(search).build());
    request.config(config.toBuilder().tools(tools).build());
    return Completable.complete();
  }
}

// Vertex AI backend: keep known content farms out of the evidence.
BaseTool search = new ConfiguredGoogleSearchTool(
    GoogleSearch.builder().excludeDomains("content-farm.example").build());

Cost and query control

According to the Gemini API documentation, grounding with Gemini 3 models is billed for each search query the model decides to run. With Gemini 2.5 and older models it is billed per prompt. Look up the current prices rather than relying on remembered ones. On a per-query model, a vague instruction that leads the model to run four queries costs four times as much as one that leads it to run one. Three things keep this under control:

  • Count queries per turn. Log the size of webSearchQueries() on every event, with the agent name and model, and alert when the 95th percentile rises.
  • Tell the model when to search. Describe in the instruction which questions need fresh facts. Questions about your own product should go to your own tools, not the web.
  • Cache answers for questions with a known freshness window, such as exchange rates or release dates. Store the answer together with its sources and the time it was fetched.

Worked example: a support agent

Consider a support agent that answers 20,000 questions a day. About one question in five is about a public fact, such as a carrier's holiday schedule. In the single-agent design, the order tool and search cannot both live on one agent, so the team wraps search with GoogleSearchAgentTool. The answers read well. A week later, a customer disputes a delivery date the agent quoted, and nobody can find the source, because the wrapper threw the metadata away.

The team replaces the wrapper with the web_searcher agent above. Every search answer now carries markers and a source list, and an after-agent check rejects answers with markers that point nowhere. The query log shows a median of two queries per searched turn and a long tail caused by vague questions. A sentence added to the instruction, "search once with the carrier's name and the date", brings the tail down. The disputed-answer rate becomes measurable, because each answer now records its sources.

Failure modes

  • Citations shifted by a few characters on non-English text: byte offsets were used as UTF-16 indices.
  • Answers with no sources after adding a second tool: AgentTool returned only the text. Inline the evidence with a callback, as shown above.
  • A built-in tool on a sub-agent: not supported in Java. Use AgentTool, not subAgents.
  • An option that is silently ignored or rejected: excludeDomains was sent to the Gemini API, or timeRangeFilter to Vertex AI.
  • Fluent answers with no grounding: the model chose not to search. Unless the instruction makes searching the expected behaviour, treat a missing groundingMetadata() as a signal for review.
  • Stale chunk indices after merging two responses: indices refer to their own response's chunk list. Renumber them when you concatenate.

Trade-offs

OptionGainsCosts
GoogleSearchTool on the agentOne line; full metadata on the eventNo other tools on that agent
GoogleSearchAgentToolMixes with function toolsDrops metadata; an extra model call
Own search agent with callbackMixes with tools and keeps sourcesExtra model call; more code
Own search API as a FunctionToolFull control of queries, filters and tracingYou run, pay for and rank the search yourself

The last option is covered in search integration for ADK Java agents. The wider set of built-in tools is mapped in Google Cloud tools in ADK Java, and per-model behaviour is covered in Gemini 2.5 features in ADK Java. Callback signatures are covered in ADK Java callbacks.

What to do next

  1. Pin your ADK and google-genai versions, and re-read GoogleSearchTool and AgentTool whenever you upgrade.
  2. Log webSearchQueries, chunk counts and the presence of grounding on every event.
  3. Render citations on the byte array, and add a unit test that uses accented and emoji text.
  4. If search shares an agent with other tools, switch to a dedicated search agent that inlines its sources.
  5. Decide on your backend before you use excludeDomains or timeRangeFilter.
  6. Read the grounding terms on displaying search suggestions, and design the UI to meet them.
Key takeaway: GoogleSearchTool only adds a googleSearch tool to the request; Gemini runs the queries and returns GroundingMetadata on the event. Render citations on UTF-8 bytes, not Java string indices. In Java a search agent cannot carry other tools, and AgentTool keeps only the final text, so inline sources with an after-model callback. Log queries per turn, because newer models bill per query.