Documentation Index
Fetch the complete documentation index at: https://trigger-docs-ai-chat-tools.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
chat.agent doesn’t call the model for you. Your tools still go to streamText inside run(). But you should also declare them on the agent config:
tools on the config does two things you can’t get by passing them to streamText alone:
- It threads your tools into the SDK’s internal message conversion, so each tool’s
toModelOutputis re-applied when prior-turn history is re-converted (seetoModelOutputacross turns). - It hands the resolved set back, typed, on the
run()payload astools, so you declare them once and don’t re-import the map.
Where tools go
There are three places a tool set shows up. Declare once, reuse:| Surface | What it’s for |
|---|---|
chat.agent({ tools }) | Re-applies toModelOutput on prior-turn history; hands the set back typed on the run() payload. |
chat.toStreamTextOptions({ tools }) | Detects which tool calls need HITL approval (needsApproval) and merges any auto-injected skill tools. |
streamText({ tools }) | What the model actually calls. chat.toStreamTextOptions({ tools }) already sets this, so spread it instead of passing tools twice. |
tools on the config, read them back from the run() payload, and pass that to chat.toStreamTextOptions({ tools }). One declaration flows everywhere.
toModelOutput across turns
toModelOutput transforms a tool’s result before it enters the model’s context, turning raw image bytes into an image content part, or compressing a long sub-agent transcript into a one-line summary. The full result still streams to the frontend; the model only sees the transformed version.
The catch is multi-turn. After each turn, chat.agent persists the conversation as UIMessage[] and re-converts it to model messages at the start of the next turn. That conversion needs your tools to find each toModelOutput. If you only pass tools to streamText and not to the config, the transform runs on turn 1 but is skipped on every later turn. The raw output gets stringified back into the prompt instead, and the model loses the transformed view.
Declaring tools on the config fixes this: the SDK threads them into the conversion, so toModelOutput is re-applied on every turn.
Static or per-turn tools
tools accepts either a static ToolSet or a function that returns one per turn, for tools that depend on the user, a feature flag, or anything in the turn context:
ResolveToolsEvent and runs once per turn (after clientData is parsed):
| Field | Type | Description |
|---|---|---|
chatId | string | The chat session ID. |
turn | number | The current turn number (0-indexed). |
continuation | boolean | Whether this run is continuing an existing chat. |
clientData | TClientData | Parsed client data from the frontend. |
run() payload’s tools.
Typed tools in run()
The run() payload’s tools is typed to whatever you declared, so you can pass it straight through without re-importing the map:
tools are declared, the payload’s tools is an empty object and behaves exactly as before, so declaring tools is fully opt-in.
Typing messages from your tools
To get typed tool parts (tool-${name} with typed input/output) on your UIMessage, in hooks like onTurnComplete and on the frontend, derive the message type from your tool set with InferChatUIMessageFromTools:
UIMessage<unknown, UIDataTypes, InferUITools<typeof tools>>. Pin it on the agent with chat.withUIMessage<ChatUiMessage>() and reuse it on the client. If you also have custom data-* parts, build the UIMessage generic directly instead. See Types.
Skills
Agent skills are auto-injected as tools (loadSkill, readFile, bash) by chat.toStreamTextOptions(). They’re separate from your config tools: declare your own tools on the config (so their toModelOutput survives across turns), and let toStreamTextOptions merge the skill tools on top at call time. Skill tools don’t define toModelOutput, so they don’t need to be on the config.
Manual turn loops (chat.customAgent)
The tools config option belongs to the managed chat.agent. When you drive the loop yourself with chat.customAgent (or build messages from chat.history), you own the conversion, so pass your tools to convertToModelMessages directly to get the same cross-turn toModelOutput behavior:
Learn more
- Human-in-the-loop: tools that pause for approval.
- Sub-agents: tools that delegate to other agents and compress their output with
toModelOutput. - Tool result auditing: logging tool results as they resolve.
- AI SDK: Tools and tool calling.

