How the Agent Toolkit works

The Agent Toolkit sits between your application (chat UI and model provider) and the TomTom Maps SDK. Each user message goes through two phases, intent classification and then a tool loop. Shared state carries context across turns.

Your ApplicationAgent Toolkit PluginMaps SDK for JavaScriptChat UI (BYO)LLM Provider (BYO)Intent ClassifierToolsState

1. Intent classification

Before the model sees any tool, one lightweight LLM call (two if the first answer fails validation) picks the tools this turn needs. Fewer tools in the request means fewer wrong choices and fewer tokens.

The default classifier reuses your main model. Give it a smaller one, or change how much history it sees:

import { createMapAgent, createDefaultClassifier } from '@tomtom-org/maps-sdk-plugin-agent-toolkit';
import { openai } from '@ai-sdk/openai';

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    classifier: createDefaultClassifier({
        model: openai('gpt-4o-mini'),
        maxHistoryMessages: 8,        // earlier messages sent (default 8)
        maxHistoryMessageLength: 300, // characters kept per earlier message (default 300)
    }),
});

Or turn it off. Every tool is then offered every turn, and scopable tools keep their full, unscoped schema:

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    classifier: false,
});

classifier also accepts your own function, (context) => Promise<ClassificationResult | null>. It receives the messages and every tool's metadata. Returning null offers every tool.

2. Tool loop

The model calls tools, such as geocode, search, route and show on map, in sequence or in parallel until it can answer.

The tool registry lists every tool.

Per-turn sequence

User messageIntent classifierprepareStep rebuilds narrowed inputSchema for scopable toolsTool loopFinal assistant replyPicks active toolsEmits per-tool scopes { analyseData: { kinds: [...] } }Model picks a tool, calls itTool execute returns summaryHeavy data lands in state 1. classify2. activeToolNames + toolScopes3. rebuilt schemas4. call5. persist 6. next step reads state7. done — compose answer

Hooking into the loop

Option Use it to
prepareStep Run your own per-step logic. Any activeTools it returns are intersected with the classifier's pick. An instructions or messages override carries over to the later steps, so build instructions from initialInstructions and messages from [...initialMessages, ...responseMessages]
maxSteps Cap the steps per turn (default 10)
providerOptions Pass provider settings such as reasoning effort to every step
stepProviderOptions Return per-step provider settings from { activeTools, stepNumber }, merged over providerOptions
outputSchemas Set false for providers that reject structured tool outputs
onToolExecute Observe each call's toolName, durationMs, isError and errorMessage
const agent = createMapAgent(map, {
    model: openai('gpt-5'),
    providerOptions: { openai: { reasoningEffort: 'low' } },
    // Think harder only on turns that write code.
    stepProviderOptions: ({ activeTools }) =>
        activeTools?.some((name) => name === 'analyseData' || name === 'processData')
            ? { openai: { reasoningEffort: 'medium' } }
            : undefined,
    onToolExecute: ({ toolName, durationMs, isError }) => console.log(toolName, durationMs, isError),
});

Run one turn at a time per agent, because scoping rewrites the agent's shared tool objects each turn, and state belongs to the agent too. Create one agent per concurrent conversation.

Per-tool classification prompts

If a custom tool is offered too often, or not when it should be, tune its classificationPrompt:

// Too broad — activates for any location question
classificationPrompt: 'Get fleet vehicle data.'

// Precise — activates only for explicit vehicle ID references
classificationPrompt: 'Locate a fleet vehicle by its ID (e.g. "TT-001"); not for general location queries.'

The system prompt never reaches the classifier, so an instruction there cannot make a tool available. The system prompt guide explains what to do instead.

Observing classification

onClassify receives each turn's pick and per-tool scopes, or null if the classifier failed open:

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    onClassify: (result) => {
        if (result) {
            console.log('Selected tools:', result.activeToolNames);
            console.log('Per-tool scopes:', result.toolScopes); // e.g. { analyseData: { kinds: ['places', 'routes'] } }
            console.log('Classification took:', result.timeMs, 'ms', result.usage.totalTokens, 'tokens');
        }
    },
});

Related