Skip to Content
Client-Side Tools

Client-Side Tools

Client-side tools are for cases where a tool can’t run on the same system that’s calling the LLM API. Instead, the tool needs to execute in the user’s environment—like a browser, a desktop app, or any other remote client. Use this when the function needs to touch client-only APIs (e.g. geolocation), update UI state, search through code on a users local machine, or interact with something the server can’t access directly.

Basic setup

Client-side tools work by having your workflow emit external-tool messages that the React useWorkflow hook intercepts and executes locally.

1. Define your tools

Start by defining your tools with schemas:

// tools/toolbox.ts import { createToolBox } from "@gensx/core"; import { z } from "zod"; export const toolbox = createToolBox({ getUserLocation: { description: "Get the user's current location using browser geolocation", params: z.object({ enableHighAccuracy: z.boolean().optional(), timeout: z.number().optional(), }), result: z.object({ latitude: z.number(), longitude: z.number(), accuracy: z.number(), }), }, moveMap: { description: "Move the map to a specific location", params: z.object({ latitude: z.number(), longitude: z.number(), zoom: z.number().optional(), }), result: z.object({ success: z.boolean(), message: z.string(), }), }, });

2. Implement tool functions

Create React hooks that implement the tool logic:

// hooks/useMapTools.ts import { ToolImplementations } from "@gensx/core"; import { toolbox } from "../tools/toolbox"; export function useMapTools() { const [mapState, setMapState] = useState({ latitude: 37.7749, longitude: -122.4194, zoom: 12, }); const toolImplementations: ToolImplementations<typeof toolbox> = { getUserLocation: { execute: async (params) => { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("Geolocation not supported")); return; } navigator.geolocation.getCurrentPosition( (position) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, }); }, (error) => reject(error), { enableHighAccuracy: params.enableHighAccuracy ?? false, timeout: params.timeout ?? 10000, } ); }); }, }, moveMap: { execute: async (params) => { setMapState({ latitude: params.latitude, longitude: params.longitude, zoom: params.zoom ?? 12, }); return { success: true, message: "Map moved" }; }, }, }; return { toolImplementations, mapState }; }

3. Connect tools to workflow

Use the useWorkflow hook with your tool implementations:

// components/ChatInterface.tsx import { useWorkflow } from "@gensx/react"; import { useMapTools } from "../hooks/useMapTools"; export function ChatInterface() { const { toolImplementations } = useMapTools(); const workflow = useWorkflow({ config: { baseUrl: "/api/gensx", }, tools: toolImplementations, }); const sendMessage = async (message: string) => { await workflow.run({ inputs: { message }, }); }; return ( <div> {/* Your chat interface */} </div> ); }

Using with AI SDK

Client-side tools work seamlessly with LLM calls. Use asToolSet to convert your toolbox to AI SDK format:

// workflows/mapWorkflow.ts import { Agent } from "./agent"; import { anthropic } from "@ai-sdk/anthropic"; import { asToolSet } from "@gensx/vercel-ai"; import { tool } from "ai"; import { z } from "zod"; import { toolbox } from "../tools/toolbox"; // Server-side tools const geocodeTool = tool({ description: "Geocode a location from an address", parameters: z.object({ address: z.string(), }), execute: async ({ address }) => { const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${address}&format=json`); return await response.json(); }, }); const MapWorkflow = gensx.Component( "MapWorkflow", async ({ userMessage }: { userMessage: string }) => { // Combine server-side and client-side tools const tools = { geocode: geocodeTool, // Server-side geocoding ...asToolSet(toolbox), // Client-side map tools }; const model = anthropic("claude-3-5-sonnet-20240620"); const result = await Agent({ messages: [ { role: "system", content: `You are a geographic assistant with access to: - geocode: Convert addresses to coordinates (server-side) - getUserLocation: Get user's current location (client-side) - moveMap: Move the map view (client-side) When users ask about locations, use geocode to find coordinates, then use moveMap to show them on the map.`, }, { role: "user", content: userMessage, }, ], tools, model, }); return result; } );

Using with OpenAI SDK

You can also use client-side tools with OpenAI models:

// workflows/openaiMapWorkflow.ts import { openai } from "@ai-sdk/openai"; import { generateText } from "ai"; import { executeExternalTool } from "@gensx/core"; import { toolbox } from "../tools/toolbox"; const OpenAIMapWorkflow = gensx.Component( "OpenAIMapWorkflow", async ({ userMessage }: { userMessage: string }) => { const result = await generateText({ model: openai("gpt-4o"), messages: [ { role: "system", content: "You are a location-aware assistant that can move maps and get user locations.", }, { role: "user", content: userMessage, }, ], tools: { moveMap: { description: "Move the map to show a specific location", parameters: z.object({ latitude: z.number(), longitude: z.number(), zoom: z.number().optional(), }), execute: async (params) => { return await executeExternalTool(toolbox, "moveMap", params); }, }, getUserLocation: { description: "Get the user's current location", parameters: z.object({ enableHighAccuracy: z.boolean().optional(), }), execute: async (params) => { return await executeExternalTool(toolbox, "getUserLocation", params); }, }, }, }); return result.text; } );

Advanced patterns

LLM-driven tool selection

Let the LLM decide which tools to use based on user queries:

const SmartMapWorkflow = gensx.Component( "SmartMapWorkflow", async ({ userMessage }: { userMessage: string }) => { const tools = { webSearch: webSearchTool, ...asToolSet(toolbox), }; const model = anthropic("claude-3-5-sonnet-20240620"); const result = await Agent({ messages: [ { role: "system", content: `You are a smart geographic assistant. Based on user queries: For location queries ("Where is X?"): 1. Use webSearch to find information about the location 2. Use moveMap to center the map on the location For "near me" queries: 1. Use getUserLocation to get their current position 2. Use webSearch to find places near them 3. Use moveMap to show results`, }, { role: "user", content: userMessage, }, ], tools, model, }); return result; } );

Tool result validation

Validate client-side tool results before using them:

const ValidatedToolWorkflow = gensx.Component( "ValidatedToolWorkflow", async ({ userMessage }: { userMessage: string }) => { const LocationSchema = z.object({ latitude: z.number().min(-90).max(90), longitude: z.number().min(-180).max(180), accuracy: z.number().positive(), }); const tools = { getUserLocation: { description: "Get user's current location", parameters: z.object({ enableHighAccuracy: z.boolean().optional(), }), execute: async (params) => { const result = await executeExternalTool(toolbox, "getUserLocation", params); return LocationSchema.parse(result); // Validate before returning }, }, }; const model = anthropic("claude-3-5-sonnet-20240620"); const result = await Agent({ messages: [ { role: "system", content: "You are a location-aware assistant with validated location data.", }, { role: "user", content: userMessage, }, ], tools, model, }); return result; } );

Best practices

Optimized tool descriptions

Write clear, specific descriptions to help LLMs use tools efficiently:

const optimizedToolbox = createToolBox({ moveMap: { description: "Move the map to center on specific coordinates. Use this when showing locations to the user.", params: z.object({ latitude: z.number().describe("Latitude coordinate (-90 to 90)"), longitude: z.number().describe("Longitude coordinate (-180 to 180)"), zoom: z.number().optional().describe("Zoom level (1-20, default 12)"), }), result: z.object({ success: z.boolean(), message: z.string(), }), }, getUserLocation: { description: "Get the user's current location using browser geolocation. Only call when you need their current position.", params: z.object({ enableHighAccuracy: z.boolean().optional().describe("Request high accuracy (uses more battery)"), }), result: z.object({ latitude: z.number(), longitude: z.number(), accuracy: z.number().describe("Accuracy in meters"), }), }, });

Complete example

Check out the full implementation in the client-side-tools example  which demonstrates:

  • Map-based chat interface
  • Real-time tool execution
  • Geolocation and geocoding tools
  • Type-safe tool definitions
  • Error handling and fallbacks
Last updated on