Skip to main content

The Single Agent Pattern

9 min read

Agentic AI is everywhere right now. Multi-step reasoning chains, tool-calling loops, orchestration frameworks. The space moves fast and the architectures keep getting more elaborate. But before you reach for a complex pipeline, it’s worth asking: what’s the simplest thing that could work?

That’s what the single agent pattern is all about. And in this post, the first in a series exploring agentic AI patterns, we’ll build one from scratch.

What is the single agent pattern?

The single agent pattern is the most minimal form of agentic AI. It consists of:

  • One LLM: a single model that receives a prompt and generates a response.
  • One or more tools: capabilities the model can invoke to ground its response in real-world data.

There’s no orchestration layer. No multi-step reasoning loop. No supervisor agent dispatching tasks to sub-agents. The model receives a query, decides whether to use its available tools, and returns a result. One call, one response.

Tool calling: the core mechanic

What makes a single agent more than just a prompt-in, text-out wrapper is tool calling. Without tools, a language model can only draw on what it learned during training. With tools, it can reach beyond its training data and interact with the outside world.

Here’s how it works in practice. You send a request to the model and declare a set of tools it’s allowed to use. The model reads the prompt, decides whether it needs external information, and if it does, it invokes one of the available tools. The tool executes, returns its result, and the model incorporates that result into its final response. All of this happens within a single API call.

User LLM Gemini 2.5 Flash Tool Google Maps 1. query 2. tool call 3. results 4. response The model receives the query, calls Google Maps for real-world data, and synthesises a grounded response. One call. One response. No orchestration.

This is the bread and butter of the single agent pattern. The model isn’t just generating text. It’s making a decision (“I need real data to answer this”), acting on that decision (calling the tool), and synthesising the result into something useful. That decision-action-synthesis loop, even when it happens just once, is what makes it agentic.

The tools themselves can be anything: a search engine, a database lookup, a calculator, a mapping service, a code interpreter. What matters is that the model knows the tool is available and can choose to use it when the task calls for it.

When is this enough?

This might sound too simple to be useful, but that’s precisely the point. Many real-world tasks are atomic: they don’t require iterative reasoning or coordination between multiple models. They just need an LLM that can reach out to an external data source and fold that information into its answer.

Think of tasks like looking up a product review, checking the weather, converting a currency, or verifying a fact. These are all single-turn problems where the model’s reasoning ability plus one well-placed tool call produces a complete, grounded answer.

In future posts, we’ll explore more sophisticated patterns: reflection loops, multi-agent systems, and beyond. But every agentic architecture builds on the foundation we’re laying here.

The project

To make this concrete, let’s build a tiny interactive agent. You start a chat session, tell it the name of a place and a postcode, and it tells you whether the place is any good, grounded in real Google Maps data.

The agent runs via Google’s Agent Development Kit (ADK), which handles the conversation loop, tool invocation, and response synthesis for you. All you do is define the agent and its tools.

The entire thing is 42 lines of TypeScript.

Try it out: click Run to see what happens behind the scenes when the agent runs:

$

Setting things up

Let’s start with the project scaffolding. Here’s the package.json:

{
  "name": "single-agent",
  "version": "1.0.0",
  "type": "module",
  "main": "agent.ts",
  "scripts": {
    "start": "npx adk run --log_level ERROR agent.ts"
  },
  "dependencies": {
    "@google/adk": "^0.5.0"
  }
}

A few things to note:

  • The start script uses npx adk run to launch the agent. ADK handles the interactive chat loop, tool invocation, and response rendering for you.
  • The --log_level ERROR flag keeps the output clean by suppressing informational logs.
  • The only dependency you need to care about is @google/adk, Google’s Agent Development Kit. It includes the GenAI SDK under the hood.

Create a .env file with your API key:

GOOGLE_API_KEY=your-api-key-here

Then install dependencies:

npm install

The code

Now for the main event. Here’s agent.ts in full:

import { LlmAgent, BaseTool, ToolProcessLlmRequest } from '@google/adk';

class GoogleMapsTool extends BaseTool {
  constructor() {
    super({ name: 'google_maps', description: 'Google Maps Tool' });
  }

  runAsync() {
    return Promise.resolve();
  }

  async processLlmRequest({ llmRequest }: ToolProcessLlmRequest) {
    llmRequest.config = llmRequest.config || {};
    llmRequest.config.tools = llmRequest.config.tools || [];
    llmRequest.config.tools.push({ googleMaps: {} });
  }
}

export const rootAgent = new LlmAgent({
  name: 'place_reviewer',
  model: 'gemini-2.5-flash-lite',
  description: 'Reviews places based on name and postcode using Google Maps.',
  instruction: `You are a place reviewer. When a user gives you a place name and postcode,
  use Google Maps grounding to look it up and provide a one-line verdict
  on whether it's any good. Be concise and opinionated.

  If the user hasn't provided a place name and postcode, ask them for both.`,
  tools: [new GoogleMapsTool()],
  afterModelCallback: async ({ response }) => {
    const chunks = response.groundingMetadata?.groundingChunks;
    if (!chunks?.length) return undefined;
    const sources = chunks
      .filter((c) => c.maps)
      .map((c) => `- ${c.maps!.title}: ${c.maps!.uri}`)
      .join('\n');
    if (!sources) return undefined;
    response.content = response.content || { role: 'model', parts: [] };
    response.content.parts = response.content.parts || [];
    response.content.parts.push({ text: `\n\nSources:\n${sources}` });
    return response;
  },
});

Let’s walk through it section by section.

The Google Maps tool

class GoogleMapsTool extends BaseTool {
  constructor() {
    super({ name: 'google_maps', description: 'Google Maps Tool' });
  }

  runAsync() {
    return Promise.resolve();
  }

  async processLlmRequest({ llmRequest }: ToolProcessLlmRequest) {
    llmRequest.config = llmRequest.config || {};
    llmRequest.config.tools = llmRequest.config.tools || [];
    llmRequest.config.tools.push({ googleMaps: {} });
  }
}

ADK tools extend BaseTool. The key method here is processLlmRequest, which runs before the request is sent to the model. It injects { googleMaps: {} } into the request’s tool configuration, telling Gemini it has access to Google Maps grounding. The runAsync method is a no-op because Google Maps grounding is handled server-side by the Gemini API; there’s nothing for us to execute locally.

The agent definition

export const rootAgent = new LlmAgent({
  name: 'place_reviewer',
  model: 'gemini-2.5-flash-lite',
  description: 'Reviews places based on name and postcode using Google Maps.',
  instruction: `You are a place reviewer. When a user gives you a place name and postcode,
  use Google Maps grounding to look it up and provide a one-line verdict
  on whether it's any good. Be concise and opinionated.

  If the user hasn't provided a place name and postcode, ask them for both.`,
  tools: [new GoogleMapsTool()],
  afterModelCallback: async ({ response }) => {
    const chunks = response.groundingMetadata?.groundingChunks;
    if (!chunks?.length) return undefined;
    const sources = chunks
      .filter((c) => c.maps)
      .map((c) => `- ${c.maps!.title}: ${c.maps!.uri}`)
      .join('\n');
    if (!sources) return undefined;
    response.content = response.content || { role: 'model', parts: [] };
    response.content.parts = response.content.parts || [];
    response.content.parts.push({ text: `\n\nSources:\n${sources}` });
    return response;
  },
});

This is the agent definition plus a post-processing callback. LlmAgent takes a name, a model, a system instruction, and a list of tools. The instruction tells the model what to do and how to behave. Notice the last line, “If the user hasn’t provided a place name and postcode, ask them for both.” This lets the agent handle the conversation naturally rather than requiring arguments upfront.

The afterModelCallback solves an important problem: Google Maps grounding metadata (source URLs, place titles) lives in the structured response, not in the model’s text output. The model can’t “see” those URLs to include them itself. This callback intercepts the response, extracts any Maps grounding chunks, and appends them as readable text so the user can see exactly where the information came from.

We export the agent as rootAgent, which is what adk run looks for as its entry point. We’re using gemini-2.5-flash-lite here: lightweight, fast, and well-suited to a focused task like this.

Why this works

It’s tempting to think that “agentic” means complex. But this example shows that a single agent with tool access can be remarkably effective when the conditions are right:

  • The task is atomic. We’re asking one question and expecting one answer. There’s no need for multi-step reasoning or iterative refinement.
  • The tool provides the missing context. The LLM is great at synthesis and judgement, but it doesn’t have live data about specific restaurants or shops. Google Maps fills that gap perfectly.
  • Tool calling is the pattern, not the specific tool. We used Google Maps grounding here, but the same approach works with any tool: a database lookup, a weather API, a code interpreter, a search engine. The single agent pattern is about giving a model one well-chosen capability and letting it decide when to use it. Google Maps just happens to be a particularly clean example.
  • ADK handles the plumbing. The conversation loop, tool invocation, and response rendering are all managed by ADK. We just define the agent and its tools, and adk run does the rest.

This is the sweet spot for the single agent pattern: tasks where a model’s reasoning ability plus one well-chosen tool is enough to produce a grounded, useful result.

Wrapping up

The single agent pattern won’t solve every problem, but it solves more than you might think. Before reaching for a framework or a multi-agent architecture, consider whether your task really needs that complexity. Often, one model and one tool is all it takes.

In upcoming posts, we’ll layer on more sophisticated patterns. But every one of them builds on the same idea: give a model the right tools and a clear objective, and let it do its work.