Skip to main content

The Single Agent Pattern

8 min read

Agentic AI architectures keep getting more elaborate. Multi-step reasoning chains, tool-calling loops, orchestration frameworks stacked three layers deep. But most tasks don’t need any of that.

The single agent pattern strips everything back to the minimum. This is the first post in a series on agentic AI patterns, and we’re starting here because every other pattern grows out of this one.

What is the single agent pattern?

It’s the most minimal agentic setup you can build. Two ingredients:

  • 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.

No orchestration layer. No multi-step reasoning loop. No supervisor dispatching tasks to sub-agents. The model receives a query, decides whether to call a tool, and returns a result. One pass.

Tool calling: the core mechanic

Without tools, a language model can only draw on what it absorbed during training. Tools let it reach outside that boundary and pull in live data.

The mechanics: you send a request to the model and declare the tools it’s allowed to use. The model reads the prompt, judges whether it needs external information, and if so, fires off a tool call. The tool executes, hands back its result, and the model folds that result into its final answer. All 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.

That sequence (decide, act, synthesise) is what makes the pattern agentic, even when it only happens once. The model spots a gap in its own knowledge, reaches for a tool to fill it, and weaves the result into a coherent response.

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 they exist and can choose when to use them.

When is this enough?

Sounds too simple? Good. That’s the point.

Many real-world tasks are atomic. They don’t need iterative reasoning or coordination between multiple models. They just need an LLM that can grab data from an external source and fold it into its answer.

Looking up a product review. Checking the weather. Converting a currency. Verifying a fact. All single-turn problems where the model’s reasoning plus one well-placed tool call produces a complete, grounded answer.

We’ll explore more sophisticated patterns in later posts (reflection loops, multi-agent systems, and beyond). But every agentic architecture sits on the foundation we’re building here.

The project

Let’s make this concrete. We’ll build a tiny interactive agent. You start a chat session, give 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. All you do is define the agent and its tools.

The whole thing is 42 lines of TypeScript.

Click Run to watch what happens behind the scenes when the agent fires:

$

Setting things up

Project scaffolding first. 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"
  }
}

Three things worth noting:

  • 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

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 method that does the work here is processLlmRequest, which fires before the request reaches the model. It injects { googleMaps: {} } into the request’s tool configuration, telling Gemini it can use Google Maps grounding. The runAsync method is a no-op because Google Maps grounding runs server-side inside the Gemini API; there’s nothing 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;
  },
});

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.” That lets the agent handle the conversation naturally rather than requiring arguments upfront.

The afterModelCallback solves a specific 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, pulls out any Maps grounding chunks, and appends them as readable text so the user sees exactly where the information came from.

We export the agent as rootAgent (that’s 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, single-turn task.

Why this works

“Agentic” doesn’t have to mean complex. This example shows that a single agent with tool access can be remarkably effective when three conditions hold:

  • The task is atomic. One question, one answer. No need for multi-step reasoning or iterative refinement.
  • The tool provides the missing context. The LLM handles synthesis and judgement well, but it doesn’t carry live data about specific restaurants or shops. Google Maps fills that gap.
  • Tool calling is the pattern, not the specific tool. We used Google Maps grounding here, but the same shape works with any tool: a database lookup, a weather API, a code interpreter, a search engine. Google Maps just happens to be a clean example.
  • ADK handles the plumbing. The conversation loop, tool invocation, and response rendering are all managed by ADK. We define the agent and its tools; adk run does the rest.

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

Wrapping up

The single agent pattern won’t solve every problem. It solves more than you’d expect, though. Before reaching for an orchestration framework or a multi-agent architecture, ask whether your task actually needs that machinery. 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 grows from the same root: give a model the right tools and a clear objective, then step back.