When a chat agent uses tools (especially human-in-the-loop tools that wait onDocumentation Index
Fetch the complete documentation index at: https://trigger.dev/docs/llms.txt
Use this file to discover all available pages before exploring further.
addToolOutput from the frontend), you often need to fire side effects exactly once per resolved tool call:
- Audit logs — record every tool result for compliance.
- Billing — charge per tool invocation.
- Notifications — alert downstream systems when a specific tool resolves.
- Search-index updates — reflect tool outputs into a derived store.
chat.history.extractNewToolResults returns.
The pattern
incomingMessages is the new wire message (0-or-1-length, see v4.5 wire format change). For each new tool result on that message, write one audit row. Then return the canonical chain from your DB.
extractNewToolResults compares the message against the current chat.history chain and returns only tool parts whose toolCallId is not already resolved. That’s what makes the call exactly-once:
- A re-emitted message (same id, same toolCallId) returns
[]— no duplicate log. - A genuinely new tool result on a known assistant message returns just the new ones.
- A first-time tool result returns the full set.
Why hydrateMessages is the right hook
The pattern works in any pre-merge callback, but hydrateMessages is the canonical spot for two reasons:
- It fires before the runtime merges the incoming message into the accumulator. Once merged, the tool results are already on the chain, and
extractNewToolResultsreturns[]for them. - It always fires per turn — including HITL turns where the user resolved a tool with
addToolOutput, which is the highest-volume audit event in most apps.
onTurnComplete fires, the chain already contains responseMessage, so calling extractNewToolResults(responseMessage) there returns []. Don’t put audit logging there for the resolution path.
Without hydrateMessages — onTurnComplete for self-emitted tool calls
If you don’t use hydrateMessages, the runtime’s snapshot+replay path handles persistence. You can still audit the agent’s own tool executions in onTurnComplete — but compare against the prior message rather than the just-emitted one:
newUIMessages is just the messages this turn produced — no prior-chain noise. Each tool part shows up exactly once.
This works for tools the agent itself calls (no HITL pause). For HITL flows where the user resolves a tool with addToolOutput, the resolution arrives on the next turn’s wire message, not in newUIMessages of the resolving turn — use hydrateMessages for those.
Idempotency at the storage layer
Even withextractNewToolResults, transient failures (e.g. an audit-log POST that times out and is retried) can produce duplicates. Make the audit-log writer idempotent on toolCallId:
toolCallId is unique per tool invocation (assigned by the AI SDK when the model emits the tool call) and stable across retries — perfect for an idempotency key.
What extractNewToolResults returns
input-available state (the model called the tool but it hasn’t resolved yet) are not returned — only resolved results count.
Combining with HITL
Human-in-the-loop tools pause the turn waiting foraddToolOutput from the frontend. When the user submits, the wire message carries an updated assistant message with the tool now in output-available state. extractNewToolResults against that message returns the just-resolved tool — exactly one audit row per user resolution:
See also
chat.history— full reference forextractNewToolResults,getPendingToolCalls,getResolvedToolCalls- Human-in-the-loop — the pattern this auditing hook complements
hydrateMessages— where pre-merge auditing lives- Persistence and replay — how the runtime rebuilds chains, and why
extractNewToolResultsworks against them

