@tomtom-org/maps-sdk-plugin-agent-toolkit
v0.4.4
Published
Agent toolkit plugin for TomTom Maps SDK JS using Vercel AI SDK
Keywords
Readme
TomTom Maps SDK - Agent Toolkit Plugin
A headless conversational agent that gives Large Language Models tool-based control over a TomTom Map and TomTom location services, powered by Vercel AI SDK v6.
No UI is included — bring your own chat interface. No LLM provider is bundled — supply any AI SDK-compatible model.
Full documentation — guides, architecture diagrams, and tutorials are available at docs.tomtom.com.
Design principles
- Client-side only — uses AI SDK's
DirectChatTransport+ToolLoopAgent. The consumer provides the model; no server infrastructure is required. - No bundled LLM provider — supply any AI SDK-compatible
LanguageModel. Keeps the package provider-agnostic. - Token-efficient — all service responses are summarized before reaching the LLM. Full GeoJSON stays in
ToolState. - Lazy module initialization — map modules are instantiated on first use and cached in state.
- Coordinate convention — always
[longitude, latitude]per GeoJSON standard, enforced throughout. - Task-oriented tools — tool boundaries follow user tasks, not SDK API surface, so a single prompt maps to a single tool call.
Installation
pnpm add @tomtom-org/maps-sdk @tomtom-org/maps-sdk-plugin-agent-toolkit ai zod maplibre-gl @turf/turf chart.js h3-jsInstall at least one AI SDK provider:
# Pick one (or more)
pnpm add @ai-sdk/openai
pnpm add @ai-sdk/anthropic
pnpm add @ai-sdk/azureQuick start
import { TomTomMap } from '@tomtom-org/maps-sdk/map';
import { createMapAgent } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
import { openai } from '@ai-sdk/openai';
// 1. Create a TomTom map
const map = new TomTomMap({
mapLibre: { container: 'map', center: [4.9, 52.4], zoom: 10 },
});
// 2. Create the agent
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
});
// 3. Send a message
const result = await agent.generate({
messages: [{ role: 'user', content: 'Find coffee shops near Dam Square, Amsterdam' }],
});
console.log(result.text);With React (useChat)
import { DirectChatTransport } from 'ai';
import { useChat } from '@ai-sdk/react';
const agent = createMapAgent(map, { model: openai('gpt-4o') });
function ChatPanel() {
const { messages, sendMessage } = useChat({
transport: new DirectChatTransport({ agent }),
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>{m.content}</div>
))}
<input onKeyDown={(e) => e.key === 'Enter' && sendMessage(e.target.value)} />
</div>
);
}See examples/map-chat-agent-react for a full working example.
Tools reference
The plugin ships a DEFAULT_TOOLS registry covering search, routing, traffic, reachable areas, BYOD GeoJSON, base-map control, MapLibre access, and code-generated analysis. All tools are included by default and can be individually removed or replaced via the tools option. The full per-tool reference lives in the Agent Toolkit guide.
Location & search
| Tool | Description |
|---|---|
| locatePlace | Resolve a location string (landmark, city, address) to a place; optionally stage as a waypoint |
| reverseGeocode | Convert [longitude, latitude] coordinates to an address |
| discoverPlaces | Search for places by text query or POI category within an area |
| getPOICategoryCodes | Look up TomTom POI category codes from natural-language names |
| getCurrentLocation | Get the user's physical GPS location from the browser |
| getViewport | Get current map center, zoom, and bounding box |
Routing & reachable areas
| Tool | Description |
|---|---|
| setRoute | Calculate or recalculate a route — provide locations (waypoints), parameters (route options), or both |
| addWaypointsToRoute | Extend the current route — prepend a new origin, append a new destination, and/or insert intermediate stops |
| removeWaypointsFromRoute | Remove one or more waypoints by index from the current route |
| replaceWaypointInRoute | Overwrite a single waypoint of the current route in place — origin, destination, or by index |
| getCurrentWaypoints | Get the staged waypoint slots (origin, stops, destination) |
| findReachableAreas | Calculate isochrone/isodistance polygons from one or more origins |
Traffic
| Tool | Description |
|---|---|
| getTrafficIncidents | Fetch traffic incidents within an area (viewport, named place, route corridor, polygon, IDs) |
| startTrafficIncidentsMonitor | Start a live monitor that refreshes incident state on a configurable cadence |
| stopTrafficIncidentsMonitor | Stop the live incident monitor |
| focusIncidents | Highlight a subset of incidents (by id / category / severity) on the map and dim the rest |
| getTrafficAreaAnalytics | Fetch historical traffic analytics (speed, congestion, travel time) for an area |
| queryTrafficAnalytics | Query cached analytics data or check what is currently displayed |
| toggleTilesTrafficFlow | Toggle the real-time traffic-flow tile overlay |
| toggleTilesTrafficIncidents | Toggle the real-time traffic-incidents tile overlay |
| getShownTileIncidents | List real-time incidents visible in the current viewport |
Bring-your-own-data (BYOD)
| Tool | Description |
|---|---|
| addByodSource | Ingest a customer-authored GeoJSON source (URL or inline FeatureCollection) and profile its shape; the entry has no layers and renders nothing until the agent sets them via setByodLayers |
| recallByod | List BYOD entries, or retrieve a single entry's full FeatureCollection by id |
| setByodLayers | Restyle a BYOD entry — replace its MapLibre layers (type + data-driven paint) using the entry's data profile |
| updateByodDisplay | Show, hide, or clear BYOD layers on the map |
See the Bring your own data guide for ingestion patterns, visibility lifecycle, and how BYOD entries feed into
analyseData/processData.
Unified data tools — scope-aware code generation
| Tool | Description |
|---|---|
| analyseData | Aggregate / chart entries (places, routes, incidents, geometries, trafficAreaAnalytics, byod) via dynamic JS; classifier-emitted scope narrows the schema per turn |
| processData | Transform entries into new places, placeConnections, geometries, byod, or a fitOnMap camera move via dynamic JS |
| executeMaplibreCode | Execute arbitrary MapLibre JS against the live Map instance — escape hatch for custom layers, animations, raster overlays |
See Code generation for the injected identifiers, output contracts, and threat model, and Scope-aware data tools for how the per-turn classifier scope keeps the prompt small.
analyseData / processData code is always isolated in the browser — a Web Worker in a sandboxed, opaque-origin iframe (no DOM/network; terminable on timeout), zero-config (the SDK lazily loads its own bundled turf/h3 for the worker). Where the code runs is chosen by environment, not configured: the browser always isolates; Node / SSR always run on the main thread (where that boundary has no equivalent — defense-in-depth only). The optional codeExecution only tunes the isolated browser run:
const agent = createMapAgent(map, {
model,
codeExecution: { timeoutMs: 5000 }, // wall-clock budget per isolated run (default 5000)
});Experimental — the isolation boundary is verified by the
e2e-tests/suite (CSP egress-block, worker termination, opaque-origin isolation), which runs in CI as a dedicated job; if the iframe can't initialise it falls back to the main thread (with a warning).turf/h3/routeUtilsare all bundled into the worker, soanalyseDataandprocessData(including route-slicing) run fully in the browser. See the code-generation guide.
Map display
| Tool | Description |
|---|---|
| updatePlacesDisplay | Show, hide, or restyle place entries on the map |
| updateRoutesDisplay | Show, hide, or restyle route entries (line color, waypoint icons, fit camera) |
| updateWaypointsDisplay | Show staged waypoint markers without the route line |
| updateTrafficAreaAnalyticsDisplay | Visualize traffic-area-analytics as hexgrid, heatmap, or tiles |
| clearMap | Remove displayed places, routes, BYOD layers, or all features |
Map control
| Tool | Description |
|---|---|
| flyTo | Move the camera to a position or bounding box |
| zoomInOrOut | Adjust the zoom level by a delta |
| setPitchBearing | Tilt (pitch) and/or rotate (bearing) the camera |
| getStandardMapStyles | List available standard map style presets |
| setMapStandardStyle | Switch map style (light, dark, satellite, driving, etc.) |
| setLanguage | Change the language for map labels and API responses |
| toggleTilesBaseMapLayerGroups | Show/hide named layer groups (buildings3D, roadLabels, water, etc.) |
| toggleTilesPOIs | Show/hide built-in map POI icons with optional category filtering |
MapLibre direct access
| Tool | Description |
|---|---|
| getMapStyleLayers | List MapLibre layer IDs with their paint/layout properties |
| setLayoutProperties | Set MapLibre layout properties on named layers |
| setPaintProperties | Set MapLibre paint properties (colors, widths, opacity) on named layers |
State & recall
| Tool | Description |
|---|---|
| recallPlaces | Retrieve the history of place lookups from this session |
| recallRoutes | Retrieve previously calculated routes from this session |
| recallRanges | Retrieve stored reachable-range results |
| recallGeometries | Look up polygon sources by { kind, id } across place footprints, isochrones, and customGeometries entries |
| recallByod | Retrieve BYOD entries (see BYOD) |
| recallState | Summarize the current contents of every state slice |
| setEntryMode | Switch a slice between multiple (default) and single entry modes |
| resetState | Reset one or all state slices |
Utilities
| Tool | Description |
|---|---|
| calculateBBox | Compute a bounding box from GeoJSON features or tool results |
| formatDistance | Format meters into a human-readable string (e.g. "2.5 km") |
| formatDuration | Format seconds into a human-readable string (e.g. "1 h 30 min") |
| help | List available capabilities in summary or searchable detail mode |
Customization
See Customizing tools for the full walkthrough — registry resolution, removing or replacing defaults, adding scopable custom tools, and starting from a blank slate.
createMapAgent options
// Define custom state by extending ToolState
interface MyState extends ToolState {
fleet: FleetState;
}
const agent = createMapAgent<MyState>(map, {
// Required: AI SDK language model instance
model: openai('gpt-4o'),
// Include built-in defaults (default: true). Set false for custom-only.
includeDefaultTools: true,
// Add, replace, or remove tools (merged with defaults)
tools: { myCustomTool: weatherTool },
// Append to the built-in system prompt
systemPromptSuffix: 'Always respond in Spanish. Use metric units.',
// Or replace it entirely (systemPromptSuffix is ignored when this is set)
systemPrompt: 'You are a delivery route planner...',
// Custom state slices — only custom fields needed, built-in slices are created automatically
state: { fleet: new FleetState() },
// Per-kind data-entry config. `enabled: false` removes the kind from the tool surface
// (drops its recall/display/fetch tools and the kind from analyseData/processData scope).
// `entryMode` switches the slice between 'multiple' (default) and 'single' (latest only).
dataEntries: {
routes: { entryMode: 'single' },
byod: { enabled: false },
},
// Intent classifier: omit for default LLM-based, false to disable
classifier: createDefaultClassifier({ model: openai('gpt-4o-mini') }),
// Observe classifier decisions
onClassify: (result) => console.log('Selected tools:', result?.activeToolNames),
// Custom prepareStep hook (composed with internal classification)
prepareStep: async (stepInfo) => ({ toolChoice: 'auto' }),
// Disable structured output schemas for providers that don't support them
outputSchemas: false,
// Max tool-loop iterations (default: 10)
maxSteps: 15,
// Opt into experimental features (subject to change without notice)
featureFlags: { experimentalSearch: true },
// Provider-specific options forwarded to the AI SDK on every step
providerOptions: {
openai: { reasoningEffort: 'low', reasoningSummary: 'auto' },
},
// Per-step providerOptions override — e.g. bump reasoning only on code-exec turns
stepProviderOptions: ({ activeTools }) =>
activeTools?.includes('processData')
? { openai: { reasoningEffort: 'medium' } }
: undefined,
});Composing tool sets
The tools option is merged with the built-in defaults. Use false to exclude a tool.
import { createMapAgent } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
// Add custom tools (defaults included automatically)
createMapAgent(map, { model, tools: { getWeather: myWeatherTool } });
// Remove specific defaults
createMapAgent(map, { model, tools: { setMapStandardStyle: false, setLanguage: false } });
// Replace a default tool (full ToolEntry required)
createMapAgent(map, { model, tools: { discoverPlaces: myCustomSearchTool } });
// Mix: add, remove, and replace in one call
createMapAgent(map, {
model,
tools: {
setLanguage: false,
discoverPlaces: myCustomSearchTool,
getWeather: myWeatherTool,
},
});
// No defaults — only custom tools
createMapAgent(map, { model, includeDefaultTools: false, tools: { myTool } });Defining custom tools
Use satisfies for type-safe custom tool definitions:
import { type ToolEntry, createMapAgent } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
import { z } from 'zod';
const fleetTools = {
trackVehicle: {
description: 'Track a vehicle by ID on the map.',
inputSchema: z.object({ vehicleId: z.string() }),
execute: async ({ vehicleId }, state) => {
// state is the full ToolState — access state.places, state.routing, etc.
const position = await fetchVehiclePosition(vehicleId);
return { vehicleId, position };
},
tags: ['fleet'],
relatedTools: ['updatePlacesDisplay'],
},
} satisfies Record<string, ToolEntry>;
// Pass directly — merged with defaults automatically
createMapAgent(map, { model, tools: fleetTools });Tool entry shape
Every tool (built-in or custom) follows the ToolEntry interface:
type ToolEntry<S extends ToolState = ToolState> = {
description: string; // Operational contract for the LLM
inputSchema: z.ZodType; // Zod schema for input validation
outputSchema?: z.ZodType; // Optional structured output schema
execute: (input, state: S) => Promise<any>;
// Classifier metadata (optional)
classificationPrompt?: string; // One-liner for the intent classifier
tags?: string[]; // Category tags (e.g. 'location', 'route')
examples?: string[]; // Code examples
examplePrompts?: string[]; // Natural language prompt examples
relatedTools?: string[]; // Tools often used together
dependsOn?: string[]; // Tools that must run before this one
};State management
See the State guide for a per-slice deep dive,
entryModesemantics (multiplevssingle), inspecting state from your application, and adding custom slices.
ToolState is organized by feature area. Each slice manages lazy-initialized map modules and an append-only history of results produced during the session. Built-in slices:
PlacesState— place / geometry entry historyRoutingState— route history, planning waypoint slots, route parametersRangeState— reachable-range entriesCustomGeometriesState— derived polygon entries produced byprocessData(union, difference, h3-coverage, …)BYODState— bring-your-own-data GeoJSON layer entriesBaseMapState— viewport, style, language, rawmapLibreMapTrafficTilesState— real-time traffic flow + incident tile-overlay visibilityTrafficAreaAnalyticsState— historical traffic-area-analytics entries and per-entry visualisationTrafficIncidentsState— fetched incident entries, registered analyses, focused subsets, and the optional polling monitorMapPOIsState— POI category visibility and filters
Built-in slices are constructed automatically by createMapAgent. To add custom slices, define an interface extending ToolState and pass the type parameter — only the custom fields need to be provided:
interface MyState extends ToolState {
fleet: FleetState;
}
const agent = createMapAgent<MyState>(map, {
model,
state: { fleet: new FleetState() },
});
agent.state.fleet; // FleetState — custom slice
agent.state.places; // PlacesState — append-only history of place entriesIntent classifier
The intent classifier is an optional per-turn optimization that selects which tools the LLM sees, reducing noise and improving accuracy. See the How it works guide for the full two-phase pipeline (classification → tool loop), prompt assembly, and observability hooks.
import { createMapAgent, createDefaultClassifier } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
// Default: uses the main model for classification
const agent = createMapAgent(map, { model: openai('gpt-4o') });
// Use a cheaper model for classification
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
classifier: createDefaultClassifier({ model: openai('gpt-4o-mini') }),
});
// Disable classification entirely (all tools always visible)
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
classifier: false,
});The classifier prompt is built dynamically from each tool's classificationPrompt and relatedTools metadata, so it stays in sync automatically when tools are added or renamed.
System prompt
The built-in BASE_SYSTEM_PROMPT covers identity, a capability summary, scope/rejection rules, response formatting, data-confidence rules, tool-execution guidance, and session-state conventions. Per-tool mechanics (coordinate order, location-reference routing, etc.) live in the tool descriptions and the classifier prompt, not here.
systemPrompt accepts either a full replacement string or a section-overrides object (Partial<Record<SystemPromptSection, string>>) — omitted sections keep their defaults, and you supply only the section body (the heading is added for you):
import {
createMapAgent,
BASE_SYSTEM_PROMPT,
composeSystemPrompt,
SYSTEM_PROMPT_SECTIONS,
} from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
// Override individual sections — pass the overrides object straight to systemPrompt
const agent = createMapAgent(map, {
model,
systemPrompt: {
identity: 'You are a delivery fleet dispatcher built on the TomTom map.',
responseFormatting: 'Reply in Dutch, metric units, one short paragraph.',
// every other section falls back to its default
},
// Section overrides still honor the prefix and suffix (a full string replacement does not).
// The prefix is prepended above the whole prompt as a heading-less preamble; the suffix is
// appended under an "ADDITIONAL INSTRUCTIONS" heading.
systemPromptPrefix: 'You work for Acme Logistics.',
systemPromptSuffix: 'Never expose internal entry ids to the user.',
});
// Append-only: keep the whole base prompt, add instructions
const agent = createMapAgent(map, { model, systemPromptSuffix: 'Always use metric units.' });
// composeSystemPrompt() does the same composition explicitly, if you need the string elsewhere
const prompt = composeSystemPrompt({ responseFormatting: 'Reply in Dutch.' });
// Extend a default section instead of replacing it: read its default body from
// SYSTEM_PROMPT_SECTIONS, derive a new value, and override with the result. Handy for
// handing a default to a coding agent to rewrite under some criteria.
const agentWithExtraRule = createMapAgent(map, {
model,
systemPrompt: {
rejectionRules: `${SYSTEM_PROMPT_SECTIONS.rejectionRules}\n- Decline weather questions.`,
},
});
// Full replacement (systemPromptPrefix and systemPromptSuffix are ignored)
const agent = createMapAgent(map, {
model,
systemPrompt: BASE_SYSTEM_PROMPT + '\n\nYou are a delivery fleet dispatcher...',
});Advanced usage
Wrapping default tools
Add logging, analytics, or custom behavior around existing tools:
import { DEFAULT_TOOLS, createMapAgent } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
const wrappedDiscover = {
...DEFAULT_TOOLS.discoverPlaces,
execute: async (input, state) => {
console.log('[Analytics] Search:', input.query);
const result = await DEFAULT_TOOLS.discoverPlaces.execute(input, state);
console.log('[Analytics] Found:', result.count, 'places');
return result;
},
};
createMapAgent(map, { model, tools: { discoverPlaces: wrappedDiscover } });Common configuration patterns
Search-only agent (no routing):
const agent = createMapAgent(map, {
model,
tools: {
setRoute: false,
addWaypointsToRoute: false,
removeWaypointsFromRoute: false,
replaceWaypointInRoute: false,
updateRoutesDisplay: false,
},
});Locked visual appearance:
const agent = createMapAgent(map, {
model,
tools: {
setMapStandardStyle: false,
setLanguage: false,
setLayoutProperties: false,
setPaintProperties: false,
toggleTilesBaseMapLayerGroups: false,
},
});The MapAgentInstance
createMapAgent returns a ToolLoopAgent (usable directly with DirectChatTransport) with two extra properties:
const agent = createMapAgent(map, { model });
agent.state; // Live state — typed as CS when custom state is provided, ToolState otherwise
agent.destroy(); // Reset all state slices (call on unmount)Public API exports
The main entry point is createMapAgent; DEFAULT_TOOLS and BASE_SYSTEM_PROMPT cover the most common customizations (see the examples above).
For the complete, always-current list of exports — factories, tool-registry helpers, state introspection, the classifier, and all types — see the API reference.
Dependencies
| Type | Package | Purpose |
|---|---|---|
| Peer | @tomtom-org/maps-sdk | TomTom Maps SDK (types, services, map modules) |
| Peer | ai@^6 | Vercel AI SDK (ToolLoopAgent, tool types) |
| Peer | zod@^4 | Schema validation |
| Peer | maplibre-gl@^5 | Map rendering engine |
| Peer | @turf/turf@^7 | Geospatial math (distance, bbox, bearing) used by the data tools |
| Peer | chart.js@^4 | Chart rendering for analyseData outputs |
| Peer | h3-js@^4 | H3 hexagonal grid for processData coverage / hexgrid visualizations |
References
- Agent Toolkit guides — full documentation set:
- How it works — two-phase pipeline (classifier + tool loop), prompt assembly, observability
- Tools — per-tool reference
- State — per-slice reference, entry modes, custom slices
- Customizing tools — add, replace, remove, or start from a blank slate
- Code generation —
analyseData/processData/executeMaplibreCode(threat model included) - Scope-aware data tools — per-turn scope mechanism
- Bring your own data — ingest customer GeoJSON layers
- AI SDK v6 documentation
- TomTom Maps SDK documentation
- Example application — full chat interface implementation
