chat.history.extractNewToolResults(message) to find incoming tool results that aren’t resolved in the current transcript. Make the resulting audit write idempotent in your database so retries can’t create duplicate records.
The helper applies to human-in-the-loop answers sent with addToolOutput. It compares the incoming message with the current history; it doesn’t track whether an external write succeeded.
The
hydrateMessages examples below apply to existing integrations. That hook is deprecated. For new conversation persistence, use transcript storage.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. For results already present in that history:
- A re-emitted message with the same resolved
toolCallIdreturns[]. - A new tool result on a known assistant message is returned.
- 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 fires on each turn, including turns that receive a user’s answer through
addToolOutput.
onTurnComplete fires, the chain already contains responseMessage, so calling extractNewToolResults(responseMessage) there returns []. Don’t put audit logging there for the resolution path.
Audit server-executed tools in onTurnComplete
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 by iterating the emitted parts and using an idempotent audit writer:
newUIMessages contains the messages this turn produced. Retries and merged assistant messages can expose a tool result again, so deduplicate these writes in your database too.
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 in existing integrations.
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 (chatId, toolCallId):
toolCallId. A new model-generated invocation can have a new ID; use an application operation ID as well when separate calls must refer to the same external action.
What extractNewToolResults returns
input-available state (the model called the tool but it hasn’t resolved yet) aren’t returned. The helper returns resolved results.
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 newly resolved tool. An idempotent audit writer keeps one row per 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

