This is the documentation of version 0.64.0. Go to the latest version, 1.0.0-rc.5.

Scope-aware data tools

analyseData and processData run model-written JavaScript over any combination of the agent's entries — 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, is thousands of tokens — paid 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.

Unscoped surfaceIntent classifiertoolScopes.analyseData = { kinds: ['places'] }Narrowed surface for this turnplacesEntryIDs, routesEntryIDs, incidentsEntryIDs, geometriesEntryIDs, trafficAreaAnalyticsEntryIDs, byodEntryIDsplacesEntryIDs only tool selectedemit scope alongside selectionprepareStep re-builds description + inputSchema

The model just sees a smaller tool for that turn. A scoped tool also gets the cross-kind cheat-sheet and the routeUtils and cluster helper docs; unscoped, it documents every enabled kind without them.

Three default tools are scopable: analyseData, processData and clusterIncidents. clusterIncidents covers incidents alone until a scope adds the kinds the query filters against, such as routes.

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 default scopable tools use this shape, limited to the kinds left enabled by dataEntries. 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. It gets two attempts, after which the tool keeps its unscoped surface for that turn — 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:

  1. A scopeSchema (Zod) — the scope shape your tool understands.
  2. A scopePrompt — a one-line hint telling the classifier how to scope the tool.
  3. A ToolEntryBuilder instead of a static ToolEntry, which the agent calls 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, so return a working unscoped entry then.

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