Skip to content

Function tools

What are function tools?

When out-of-the-box tools don't fully meet specific requirements, developers can create custom function tools. This allows for tailored functionality, such as connecting to proprietary databases or implementing unique algorithms.

For example, a function tool, "myfinancetool", might be a function that calculates a specific financial metric. ADK also supports long running functions, so if that calculation takes a while, the agent can continue working on other tasks.

ADK offers several ways to create functions tools, each suited to different levels of complexity and control:

  1. Function Tool
  2. Long Running Function Tool
  3. Agents-as-a-Tool

1. Function Tool

Transforming a function into a tool is a straightforward way to integrate custom logic into your agents. This approach offers flexibility and quick integration.

Parameters

Define your function parameters using standard JSON-serializable types (e.g., string, number, array, object). It's important to avoid setting default values for parameters, as the language model (LLM) does not currently support interpreting them.

Return Type

The preferred return type for a TypeScript Function Tool is an object. This allows you to structure the response with key-value pairs, providing context and clarity to the LLM. If your function returns a type other than an object, the framework automatically wraps it into an object with a single key named "result".

Strive to make your return values as descriptive as possible. For example, instead of returning a numeric error code, return an object with an "errorMessage" key containing a human-readable explanation. Remember that the LLM, not a piece of code, needs to understand the result. As a best practice, include a "status" key in your return object to indicate the overall outcome (e.g., "success", "error", "pending"), providing the LLM with a clear signal about the operation's state.

JSDoc Comments

The JSDoc comments of your function serve as the tool's description and are sent to the LLM. Therefore, a well-written and comprehensive JSDoc comment is crucial for the LLM to understand how to use the tool effectively. Clearly explain the purpose of the function, the meaning of its parameters, and the expected return values.

Example

This tool is a TypeScript function which obtains the stock price of a given stock ticker/symbol.

Note: You need to install the Yahoo Finance library before using this tool: npm install yahoo-finance2

/**
 * TypeScript port of the Function Tool example from the Python ADK library
 * 
 * This example demonstrates how to use a function as a tool for an agent.
 * Note: The original example used yfinance which is a Python package for stock data.
 * In this TypeScript port, we'll simulate the stock data function instead.
 * 
 * NOTE: This is a template file that demonstrates how to use the ADK TypeScript library.
 * You'll see TypeScript errors in your IDE until you install the actual 'adk-typescript' package.
 * The structure and patterns shown here match how you would use the library in a real project.
 */

import { 
  Agent, 
  Runner,
  Content,
  InMemorySessionService,
  FunctionTool
} from 'adk-typescript';

// Constants for the app
const APP_NAME = "stock_app";
const USER_ID = "1234";
const SESSION_ID = "session1234";

// Configure logging (simplified version for TypeScript)
const logger = {
  info: (message: string, ...args: any[]) => console.info(message, ...args),
  error: (message: string, ...args: any[]) => console.error(message, ...args)
};

/**
 * Retrieves the current stock price for a given symbol.
 * This is a simulated version of the Python yfinance function.
 * 
 * @param symbol The stock symbol (e.g., "AAPL", "GOOG").
 * @returns The current stock price, or null if an error occurs.
 */
function getStockPrice(symbol: string): number | null {
  try {
    // Simulated stock prices
    const stockPrices: Record<string, number> = {
      "AAPL": 187.32,
      "GOOG": 141.18,
      "MSFT": 378.85,
      "AMZN": 175.47,
      "META": 471.05,
      "TSLA": 177.86,
      "NVDA": 824.98
    };

    // In a real implementation, this would make an API call
    // to a financial data provider or use a library like yfinance
    const price = stockPrices[symbol.toUpperCase()];

    if (price !== undefined) {
      console.log(`Retrieved stock price for ${symbol}: $${price}`);
      return price;
    } else {
      console.log(`Could not find stock price for ${symbol}`);
      return null;
    }
  } catch (error) {
    console.error(`Error retrieving stock price for ${symbol}:`, error);
    return null;
  }
}

// Create the agent with the function tool
const stockPriceAgent = new Agent("stock_agent", {
  model: "gemini-2.0-flash",
  instruction: `You are an agent who retrieves stock prices. If a ticker symbol is provided, fetch the current price. If only a company name is given, first perform a Google search to find the correct ticker symbol before retrieving the stock price. If the provided ticker symbol is invalid or data cannot be retrieved, inform the user that the stock price could not be found.`,
  description: `This agent specializes in retrieving real-time stock prices. Given a stock ticker symbol (e.g., AAPL, GOOG, MSFT) or the stock name, use the tools and reliable data sources to provide the most up-to-date price.`,
  tools: [getStockPrice] // Add the function directly - it will be wrapped as a FunctionTool
});

// Create Session and Runner
const sessionService = new InMemorySessionService();
const session = sessionService.createSession({
  appName: APP_NAME, 
  userId: USER_ID, 
  sessionId: SESSION_ID
});

const runner = new Runner({
  agent: stockPriceAgent, 
  appName: APP_NAME, 
  sessionService: sessionService
});

// Agent Interaction function
function callAgent(query: string): void {
  // Create content for the request
  const content: Content = {
    role: 'user',
    parts: [{ text: query }]
  };

  // Run the agent and collect results
  (async () => {
    try {
      const events = runner.run({
        userId: USER_ID, 
        sessionId: SESSION_ID, 
        newMessage: content
      });

      for await (const event of events) {
        if (event.isFinalResponse && event.content && event.content.parts && event.content.parts[0].text) {
          const finalResponse = event.content.parts[0].text;
          console.log("Agent Response: ", finalResponse);
        }
      }
    } catch (error) {
      console.error("Error running agent:", error);
    }
  })();
}

// Execute with a sample query
callAgent("stock price of GOOG");

// Export for external use
export const agent = stockPriceAgent;
export function runStockPriceDemo(query: string): void {
  callAgent(query);
} 

The return value from this tool will be wrapped into an object if it's not already one:

{"result": "$123.45"}

Best Practices

While you have considerable flexibility in defining your function, remember that simplicity enhances usability for the LLM. Consider these guidelines:

  • Fewer Parameters are Better: Minimize the number of parameters to reduce complexity.
  • Simple Data Types: Favor primitive data types like string and number over custom classes whenever possible.
  • Meaningful Names: The function's name and parameter names significantly influence how the LLM interprets and utilizes the tool. Choose names that clearly reflect the function's purpose and the meaning of its inputs. Avoid generic names like doStuff().

2. Long Running Function Tool

Designed for tasks that require a significant amount of processing time without blocking the agent's execution. This tool is a specialized version of FunctionTool.

When using a LongRunningFunctionTool, your TypeScript function can initiate the long-running operation and optionally return an intermediate result to keep the model and user informed about the progress. The agent can then continue with other tasks. An example is the human-in-the-loop scenario where the agent needs human approval before proceeding with a task.

How it Works

You wrap a TypeScript async generator function (a function using async function* and yield) with LongRunningFunctionTool.

  1. Initiation: When the LLM calls the tool, your generator function starts executing.

  2. Intermediate Updates (yield): Your function should yield intermediate JavaScript objects (typically objects) periodically to report progress. The ADK framework takes each yielded value and sends it back to the LLM packaged within a FunctionResponse. This allows the LLM to inform the user (e.g., status, percentage complete, messages).

  3. Completion (return): When the task is finished, the generator function uses return to provide the final JavaScript object result.

  4. Framework Handling: The ADK framework manages the execution. It sends each yielded value back as an intermediate FunctionResponse. When the generator completes, the framework sends the returned value as the content of the final FunctionResponse, signaling the end of the long-running operation to the LLM.

Creating the Tool

Define your generator function and wrap it using the LongRunningFunctionTool class:

import { LongRunningFunctionTool } from 'adk-typescript';

// Define your generator function (see example below)
async function* myLongTaskGenerator(...args: any[]): AsyncGenerator<any, any, unknown> {
  // ... setup ...
  yield { status: "pending", message: "Starting task..." }; // Framework sends this as FunctionResponse
  // ... perform work incrementally ...
  yield { status: "pending", progress: 50 };               // Framework sends this as FunctionResponse
  // ... finish work ...
  return { status: "completed", result: "Final outcome" }; // Framework sends this as final FunctionResponse
}

// Wrap the function
const myTool = new LongRunningFunctionTool({
  func: myLongTaskGenerator
});

Intermediate Updates

Yielding structured JavaScript objects is crucial for providing meaningful updates. Include keys like:

  • status: e.g., "pending", "running", "waiting_for_input"

  • progress: e.g., percentage, steps completed

  • message: Descriptive text for the user/LLM

  • estimatedCompletionTime: If calculable

Each value you yield is packaged into a FunctionResponse by the framework and sent to the LLM.

Final Result

The JavaScript object your generator function returns is considered the final result of the tool execution. The framework packages this value (even if it's null or undefined) into the content of the final FunctionResponse sent back to the LLM, indicating the tool execution is complete.

Example: File Processing Simulation
/**
 * TypeScript port of the Long Running Function Tool example from the Python ADK library
 * 
 * This example demonstrates how to use a LongRunningFunctionTool to process files
 * with progress updates during execution.
 * 
 * NOTE: This is a template file that demonstrates how to use the ADK TypeScript library.
 * You'll see TypeScript errors in your IDE until you install the actual 'adk-typescript' package.
 * The structure and patterns shown here match how you would use the library in a real project.
 */

import { 
  Agent, 
  Runner,
  Content,
  InMemorySessionService,
  LongRunningFunctionTool
} from 'adk-typescript';

// Constants for the app
const APP_NAME = "file_processor";
const USER_ID = "1234";
const SESSION_ID = "session1234";

// Configure logging (simplified version for TypeScript)
const logger = {
  info: (message: string, ...args: any[]) => console.info(message, ...args),
  error: (message: string, ...args: any[]) => console.error(message, ...args)
};

// 1. Define the generator function
async function* processLargeFile(filePath: string): AsyncGenerator<Record<string, string>, Record<string, string>, void> {
  /**
   * Simulates processing a large file, yielding progress updates.
   * 
   * @param filePath Path to the file being processed.
   * @returns A final status dictionary.
   */
  const totalSteps = 5;

  // This object will be sent in the first FunctionResponse
  yield { status: "pending", message: `Starting processing for ${filePath}...` };

  for (let i = 0; i < totalSteps; i++) {
    // Simulate work for one step
    await new Promise(resolve => setTimeout(resolve, 1000));

    const progress = (i + 1) / totalSteps;
    // Each yielded object is sent in a subsequent FunctionResponse
    yield {
      status: "pending",
      progress: `${Math.floor(progress * 100)}%`,
      estimated_completion_time: `~${totalSteps - (i + 1)} seconds remaining`
    };
  }

  // This returned object will be sent in the final FunctionResponse
  return { status: "completed", result: `Successfully processed file: ${filePath}` };
}

// 2. Wrap the function with LongRunningFunctionTool
const longRunningTool = new LongRunningFunctionTool(processLargeFile);

// 3. Use the tool in an Agent
const fileProcessorAgent = new Agent("file_processor_agent", {
  // Use a model compatible with function calling
  model: "gemini-2.0-flash",
  instruction: `You are an agent that processes large files. When the user provides a file path, use the 'process_large_file' tool. Keep the user informed about the progress based on the tool's updates (which arrive as function responses). Only provide the final result when the tool indicates completion in its final function response.`,
  tools: [longRunningTool]
});

// Create Session and Runner
const sessionService = new InMemorySessionService();
const session = sessionService.createSession({
  appName: APP_NAME, 
  userId: USER_ID, 
  sessionId: SESSION_ID
});

const runner = new Runner({
  agent: fileProcessorAgent, 
  appName: APP_NAME, 
  sessionService: sessionService
});

// Agent Interaction function
function callAgent(query: string): void {
  // Create content for the request
  const content: Content = {
    role: 'user',
    parts: [{ text: query }]
  };

  // Run the agent and collect results
  (async () => {
    try {
      const events = runner.run({
        userId: USER_ID, 
        sessionId: SESSION_ID, 
        newMessage: content
      });

      for await (const event of events) {
        if (event.isFinalResponse && event.content && event.content.parts && event.content.parts[0].text) {
          const finalResponse = event.content.parts[0].text;
          console.log("Agent Response: ", finalResponse);
        }
      }
    } catch (error) {
      console.error("Error running agent:", error);
    }
  })();
}

// Execute with a sample query
callAgent("Please process the file at /path/to/example.txt");

// Export for external use
export const agent = fileProcessorAgent;
export function runFileProcessorDemo(filePath: string): void {
  callAgent(`Please process the file at ${filePath}`);
} 

Key aspects of this example

  • processLargeFile: This generator simulates a lengthy operation, yielding intermediate status/progress objects.

  • LongRunningFunctionTool: Wraps the generator; the framework handles sending yielded updates and the final return value as sequential FunctionResponses.

  • Agent instruction: Directs the LLM to use the tool and understand the incoming FunctionResponse stream (progress vs. completion) for user updates.

  • Final return: The function returns the final result object, which is sent in the concluding FunctionResponse to indicate completion.

3. Agent-as-a-Tool

This powerful feature allows you to leverage the capabilities of other agents within your system by calling them as tools. The Agent-as-a-Tool enables you to invoke another agent to perform a specific task, effectively delegating responsibility. This is conceptually similar to creating a TypeScript function that calls another agent and uses the agent's response as the function's return value.

Key difference from sub-agents

It's important to distinguish an Agent-as-a-Tool from a Sub-Agent.

  • Agent-as-a-Tool: When Agent A calls Agent B as a tool (using Agent-as-a-Tool), Agent B's answer is passed back to Agent A, which then summarizes the answer and generates a response to the user. Agent A retains control and continues to handle future user input.

  • Sub-agent: When Agent A calls Agent B as a sub-agent, the responsibility of answering the user is completely transferred to Agent B. Agent A is effectively out of the loop. All subsequent user input will be answered by Agent B.

Usage

To use an agent as a tool, wrap the agent with the AgentTool class.

import { AgentTool } from 'adk-typescript';

tools: [new AgentTool({ agent: agentB })]

Customization

The AgentTool class provides the following attributes for customizing its behavior:

  • skipSummarization: boolean: If set to true, the framework will bypass the LLM-based summarization of the tool agent's response. This can be useful when the tool's response is already well-formatted and requires no further processing.
Example
/**
 * TypeScript port of the Summarizer Agent Tool example from the Python ADK library
 * 
 * This example demonstrates how to use an Agent as a tool for another Agent.
 * 
 * NOTE: This is a template file that demonstrates how to use the ADK TypeScript library.
 * You'll see TypeScript errors in your IDE until you install the actual 'adk-typescript' package.
 * The structure and patterns shown here match how you would use the library in a real project.
 */

import { 
  Agent, 
  Runner,
  Content,
  InMemorySessionService,
  AgentTool
} from 'adk-typescript';

// Constants for the app
const APP_NAME = "summary_agent";
const USER_ID = "user1234";
const SESSION_ID = "1234";

// Configure logging (simplified version for TypeScript)
const logger = {
  info: (message: string, ...args: any[]) => console.info(message, ...args),
  error: (message: string, ...args: any[]) => console.error(message, ...args)
};

// Create the summary agent
const summaryAgent = new Agent("summary_agent", {
  model: "gemini-2.0-flash",
  instruction: `You are an expert summarizer. Please read the following text and provide a concise summary.`,
  description: "Agent to summarize text"
});

// Create the root agent that will use the summary agent as a tool
const rootAgent = new Agent("root_agent", {
  model: "gemini-2.0-flash",
  instruction: `You are a helpful assistant. When the user provides a text, use the 'summarize' tool to generate a summary. Always forward the user's message exactly as received to the 'summarize' tool, without modifying or summarizing it yourself. Present the response from the tool to the user.`,
  tools: [new AgentTool(summaryAgent)]
});

// Create Session and Runner
const sessionService = new InMemorySessionService();
const session = sessionService.createSession({
  appName: APP_NAME, 
  userId: USER_ID, 
  sessionId: SESSION_ID
});

const runner = new Runner({
  agent: rootAgent, 
  appName: APP_NAME, 
  sessionService: sessionService
});

// Agent Interaction function
function callAgent(query: string): void {
  // Create content for the request
  const content: Content = {
    role: 'user',
    parts: [{ text: query }]
  };

  // Run the agent and collect results
  (async () => {
    try {
      const events = runner.run({
        userId: USER_ID, 
        sessionId: SESSION_ID, 
        newMessage: content
      });

      for await (const event of events) {
        if (event.isFinalResponse && event.content && event.content.parts && event.content.parts[0].text) {
          const finalResponse = event.content.parts[0].text;
          console.log("Agent Response: ", finalResponse);
        }
      }
    } catch (error) {
      console.error("Error running agent:", error);
    }
  })();
}

// Example long text to summarize
const longText = `Quantum computing represents a fundamentally different approach to computation, 
leveraging the bizarre principles of quantum mechanics to process information. Unlike classical computers 
that rely on bits representing either 0 or 1, quantum computers use qubits which can exist in a state of superposition - effectively 
being 0, 1, or a combination of both simultaneously. Furthermore, qubits can become entangled, 
meaning their fates are intertwined regardless of distance, allowing for complex correlations. This parallelism and 
interconnectedness grant quantum computers the potential to solve specific types of incredibly complex problems - such 
as drug discovery, materials science, complex system optimization, and breaking certain types of cryptography - far 
faster than even the most powerful classical supercomputers could ever achieve, although the technology is still largely in its developmental stages.`;

// Run the agent with the example text
callAgent(longText);

// Export for external use
export const agent = rootAgent;
export function runSummarizerDemo(text: string): void {
  callAgent(text);
} 

How it works

  1. When the mainAgent receives the long text, its instruction tells it to use the 'summarize' tool for long texts.
  2. The framework recognizes 'summarize' as an AgentTool that wraps the summaryAgent.
  3. Behind the scenes, the mainAgent will call the summaryAgent with the long text as input.
  4. The summaryAgent will process the text according to its instruction and generate a summary.
  5. The response from the summaryAgent is then passed back to the mainAgent.
  6. The mainAgent can then take the summary and formulate its final response to the user (e.g., "Here's a summary of the text: ...")