Customizing tools
Every deployment shapes the built-in registry differently: remove tools users shouldn't trigger,
replace a default with one wired to your backend, add domain tools, or start from a blank slate and hand-pick
built-ins. The four patterns compose in one createMapAgent call.
How the registry is resolved
Three sources, in this order, make the tool set the agent exposes:
DEFAULT_TOOLS— the 55 built-in tools, unlessincludeDefaultTools: false.dataEntries— a disabled kind drops its tools, as listed inTOOLS_BY_DATA_ENTRY_KIND, and leaves the data tools' inputs.byod: { enabled: false }removesaddByodSource,setByodLayersandupdateByodDisplay.- Your
toolsoption — per-key overrides. They win over both, so a tool you pass under a dropped name comes back.
Remove a default tool
Pass false against the tool's key to drop it from the registry. Use this when a tool is irrelevant for your use case (e.g. language switching in a single-language deployment) or actively harmful (e.g. executeMaplibreCode in an untrusted-prompt deployment).
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
tools: {
setLanguage: false,
executeMaplibreCode: false,
},
});
Removed tools are invisible to the classifier and cannot be called by the model.
Replace a default tool
Pass a full ToolEntry under a built-in's key; the built-in is never instantiated. The usual case is wiring a built-in
to your backend — here getCurrentLocation reads the fleet system instead of the browser's geolocation API:
import { z } from 'zod';
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
tools: {
getCurrentLocation: {
description: 'Returns the user position from the company fleet system.',
inputSchema: z.object({}),
execute: async () => {
const position = await fleetApi.getDriverPosition();
return { position };
},
classificationPrompt: 'Get the driver\'s current GPS position from the fleet system.',
tags: ['location'],
},
},
});
Keep the original's input and output shape where you can — other tool descriptions and the system prompt may assume it.
If yours differs, say so in its description or your systemPromptSuffix, so the model doesn't pass the old fields.
Add a custom tool
Custom tools receive the same state as the built-ins, so they can hand results to the built-in display
flow — places through state.places.addPlaceResult, your GeoJSON through state.byod.addEntry.
import { z } from 'zod';
import type { Place } from '@tomtom-org/maps-sdk/core';
import type { ToolEntry } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
const getFleetVehicle: ToolEntry = {
description: 'Get the current map position of a fleet vehicle by ID and add it to the places history.',
classificationPrompt: 'Locate or display a fleet vehicle on the map by its ID.',
inputSchema: z.object({ vehicleId: z.string().describe('The fleet vehicle identifier') }),
execute: async ({ vehicleId }, state) => {
const position = await fleetApi.getPosition(vehicleId);
const place: Place = {
type: 'Feature',
id: `vehicle-${vehicleId}`,
geometry: { type: 'Point', coordinates: position },
properties: {
type: 'POI',
address: { freeformAddress: '' },
poi: { name: `Vehicle ${vehicleId}`, categories: [], localizedCategories: [] },
},
};
// Append to the places history; the model can then show it via updatePlacesDisplay,
// route to it via setRoute, or recall it later through recallState.
const entryId = await state.places.addPlaceResult(place, `Vehicle ${vehicleId}`);
return { vehicleId, entryId, position };
},
tags: ['location'],
examplePrompts: ['Where is vehicle TT-001?', 'Show fleet vehicle on the map'],
};
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
tools: { getFleetVehicle },
});
The tool takes part in intent classification through its
classificationPrompt — the classifier reads that line, not the description. A tool whose schema should change with
what the user asked is a scopable custom tool.
Resolve a place in a custom tool
A custom tool that needs one place by name can run the same lookup as the built-in locatePlace tool:
locatePlace(query, queryAs, bias?, execOptions?)returns the best-matchingPlace, ornull.queryAsis'poi'to search points of interest by name, or'place'to geocode an address or an area. Pass theexecuteoptions asexecOptions, so a cancelled turn cancels the request.getViewportBias(state.baseMap)returns the map centre as[longitude, latitude], to favour results the user is looking at. Below zoomMIN_VIEWPORT_BIAS_ZOOM(6) it returnsundefined, as the view is too wide to say where the user means.
import { getViewportBias, locatePlace, type ToolEntry } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
import { z } from 'zod';
const findStore: ToolEntry = {
description: 'Find one of our stores by name, near the map view when zoomed in, and add it to the places history.',
classificationPrompt: 'Locate a named store of ours.',
inputSchema: z.object({ storeName: z.string() }),
execute: async ({ storeName }, state, options) => {
try {
const position = getViewportBias(state.baseMap);
const place = await locatePlace(storeName, 'poi', position && { position }, options);
if (!place) return { status: 'not_found' };
return { entryId: await state.places.addPlaceResult(place, storeName) };
} catch (error) {
return { error: `Store lookup failed: ${error instanceof Error ? error.message : String(error)}` };
}
},
};
A failed or cancelled lookup rejects, so execute catches it and returns an error shape, as the next section explains.
Errors are part of the contract
Every execute catches its own failures and returns an error shape, typically { error: string }; a throw surfaces as
an opaque step failure the model can't reason about. Name the missing entity and the next valid action:
execute: async ({ vehicleId }, state) => {
try {
const position = await fleetApi.getPosition(vehicleId);
// …
} catch (error) {
return { error: `Fleet vehicle "${vehicleId}" not found. Call listFleetVehicles to see available IDs.` };
}
}
- When the model must branch on the failure (retry, call something else, give up), return a discriminated status —
{ status: 'not_found' | 'unauthorized' | … }— rather than prose. - Tell input errors (unknown id: call something else first) from external ones (a service 5xx: safe to retry once). Vague error strings are the main cause of retry loops.
- A cancelled turn is not a failure: catch
SDKAbortErrorseparately.
Start from a blank slate
Set includeDefaultTools: false for a narrowly scoped agent that should not stray into general map control, and re-add
the built-ins you want from DEFAULT_TOOLS. tools takes any tool definition, an entry or a builder:
import { createMapAgent, DEFAULT_TOOLS } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
includeDefaultTools: false,
tools: {
getFleetVehicle, // your custom tool
locatePlace: DEFAULT_TOOLS.locatePlace, // re-add built-ins
flyTo: DEFAULT_TOOLS.flyTo,
analyseData: DEFAULT_TOOLS.analyseData,
},
});
dataEntries still narrows the data tools you re-add, such as analyseData. The
site selection agent example builds its agent this way.
Putting it all together
A real deployment usually mixes all four patterns:
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
// Narrow the data surface — drops every byod-related default tool.
dataEntries: {
byod: { enabled: false },
routes: { entryMode: 'single' },
},
// Per-key overrides on top of what dataEntries left.
tools: {
// Remove what doesn't fit the deployment
setLanguage: false,
executeMaplibreCode: false,
// Replace with a backend-backed version
getCurrentLocation: fleetCurrentLocationTool,
// Add new domain tools
getFleetVehicle,
getDispatchJobs,
},
});
Verifying your registry
Use onClassify to confirm the right tools survive resolution and the classifier picks up your custom tools:
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
tools: { getFleetVehicle },
onClassify: (result) => {
console.log('Tools selected for this turn:', result?.activeToolNames);
},
});
If a custom tool isn't activating when expected, rework its classificationPrompt: too vague and it fires every turn,
too narrow and it never does. alwaysActive: true offers it every step regardless.
Related
- Tool registry — the full list of built-in tools and what each one does.
- Scope-aware data tools — building custom tools whose schema changes per turn.
- Bring your own data — when your "data" is a GeoJSON layer rather than a new tool.
- State — extending agent state alongside a custom tool.
- Securing your agent — giving custom tools narrow schemas, and keeping side effects off the model's word.