Scope-aware data tools
analyseData and processData run model-written JavaScript over any combination of the agent’s places,
routes, traffic incidents, custom polygons, traffic area analytics and your own GeoJSON. Each entry kind reaches the code
as one per-entry record, such as placesByEntry. Code generation
lists them all. For your own GeoJSON, the model picks property keys from the entry’s
data profile rather than guessing.
Documenting every kind makes these tools large. Scope narrows each tool to the kinds a turn touches. This page explains how, and how to make your own tools scopable.
Why scope exists
A tool that documents all six input kinds, with each kind’s schema, costs thousands of tokens on every turn that
offers it, even when the turn touches one kind. When a user asks “chart the places by category”, only places matters,
so the other five kinds needn’t appear at all.
The model sees a smaller tool for that turn. Scoped or not, the helper docs follow the active kinds. The cross-kind
cheat-sheet appears when more than one is active. routeUtils appears with routes, and cluster with incidents.
Three default data tools are scopable: analyseData, processData and clusterIncidents. clusterIncidents covers
incidents alone until a scope adds the kinds the query filters against, such as routes.
setMapStyling scopes the same way, by knob family rather than data kind. Its families are look, baseMap, pois, trafficFlow,
trafficIncidents and terrain for the map’s own, and places, routes, geometries, ranges,
trafficAreaAnalytics and trafficIncidentDetails for the layers the agent drew. Its set parameter then lists only those families’ knob ids, though any id stays
settable. The tool refuses an unknown id and names the closest ones it resembles.
The scope shape
import type { EntryDataKind } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
// EntryDataKind = 'places' | 'routes' | 'incidents'// | 'customGeometries' | 'trafficAreaAnalytics' | 'byod'
type DataToolScope = { kinds: readonly EntryDataKind[] };All three scopable data tools use this shape, limited to the kinds left enabled by
dataEntries. setMapStyling takes the same { kinds } with its knob
families. EntryDataKind names data kinds, which differ from the state-slice keys in EntryModeSliceName
(routes vs routing).
Mandatory scope
The classifier must emit a valid scope for every scopable tool it picks. An answer without one fails validation. After two failed attempts, the tool keeps its unscoped surface for that turn, which is larger but fully functional.
With classifier: false, scoping never engages and the tools always use their unscoped surface.
Observing scopes
const agent = createMapAgent(map, { model: openai('gpt-4o'), onClassify: (result) => { console.log('tools:', result?.activeToolNames); console.log('scopes:', result?.toolScopes); // e.g. { analyseData: { kinds: ['places', 'routes'] } } },});Watch toolScopes across a few turns to confirm the classifier picks minimal kind sets.
Add a scopable custom tool
Custom tools opt into the same narrowing with three pieces:
- A
scopeSchema(Zod) with the scope shape your tool understands. - A one-line
scopePromptthat tells the classifier how to scope the tool. - A
ToolEntryBuilderinstead of a staticToolEntry. The agent calls it again each turn with the parsed scope.
import { z } from 'zod';import type { ToolEntryBuilder } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
type FleetScope = { kinds: ('vehicles' | 'jobs')[] };const fleetScopeSchema = z.object({ kinds: z.array(z.enum(['vehicles', 'jobs'])).min(1) });
const fleetAnalyseBuilder: ToolEntryBuilder<MyState, FleetScope> = ({ scope }) => ({ description: scope ? `Analyse ${scope.kinds.join(' + ')} entries via dynamic JS.` : 'Analyse fleet data.', inputSchema: buildFleetSchema(scope), // narrowed when scope is set execute: executeFleetAnalyse, scopeSchema: fleetScopeSchema, scopePrompt: 'Emit `{ kinds: ["vehicles" | "jobs"] }` listing only the kinds the user query touches.',});
createMapAgent<MyState>(map, { model: openai('gpt-4o'), state: { fleet: new FleetState() }, tools: { fleetAnalyse: fleetAnalyseBuilder },});The classifier reads the scopePrompt and then requires a scope whenever it picks the tool. scope is undefined when
the agent is created and on turns without a scope. For those, return a working unscoped entry.
Why builder, not static entry
Only a builder can be rebuilt per turn. A static ToolEntry with a scopeSchema keeps its full schema whatever scope
the classifier emits.
Related
- Code generation — the runtime, sandbox and threat model for
analyseData,processDataand the other code-generation tools. - How it works — where in the turn the classifier emits scopes.
- Customizing tools — non-scopable custom tools.
- State — what each entry kind looks like in state.
- Securing your agent — what the toolkit bounds per turn, and what needs a server.