Skip to main content
Use 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

The hook fires per turn. 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 toolCallId returns [].
  • 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:
  1. It fires before the runtime merges the incoming message into the accumulator. Once merged, the tool results are already on the chain, and extractNewToolResults returns [] for them.
  2. It fires on each turn, including turns that receive a user’s answer through addToolOutput.
By the time 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 with extractNewToolResults, 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):
A replay of the same tool call keeps its 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

Tool parts in 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 for addToolOutput 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 the HITL pattern’s net-new-tool-result section.

See also