@@ -44,6 +44,8 @@ import {
4444 convertToModelMessages ,
4545 dynamicTool ,
4646 generateId as generateMessageId ,
47+ getToolName ,
48+ isToolUIPart ,
4749 jsonSchema ,
4850 JSONSchema7 ,
4951 Schema ,
@@ -1411,20 +1413,197 @@ function getChatHistoryState(): UIMessage[] {
14111413}
14121414
14131415/**
1414- * Imperative API for modifying the accumulated message history.
1416+ * A tool call surfaced by `chat.history.getPendingToolCalls()` /
1417+ * `getResolvedToolCalls()`. Identifies the call by its `toolCallId` plus
1418+ * the `messageId` of the assistant message that hosts it, so callers can
1419+ * locate the part precisely without re-walking the chain.
1420+ */
1421+ export type ChatToolCallRef = {
1422+ toolCallId : string ;
1423+ toolName : string ;
1424+ messageId : string ;
1425+ } ;
1426+
1427+ /**
1428+ * A new tool result surfaced by `chat.history.extractNewToolResults()`.
1429+ * `errorText` is set iff the part is in `output-error` state; otherwise
1430+ * `output` carries the resolved value.
1431+ */
1432+ export type ChatNewToolResult = {
1433+ toolCallId : string ;
1434+ toolName : string ;
1435+ output : unknown ;
1436+ errorText ?: string ;
1437+ } ;
1438+
1439+ /**
1440+ * Tool parts that are "done" — either succeeded with a value or failed
1441+ * with an error. Excludes pending (`input-streaming`/`input-available`)
1442+ * and approval (`approval-requested`/`approval-responded`) states.
1443+ * @internal
1444+ */
1445+ function isResolvedToolState ( state : unknown ) : state is "output-available" | "output-error" {
1446+ return state === "output-available" || state === "output-error" ;
1447+ }
1448+
1449+ /** @internal */
1450+ function isPendingToolState ( state : unknown ) : state is "input-available" {
1451+ return state === "input-available" ;
1452+ }
1453+
1454+ /**
1455+ * Walk an assistant message and yield each tool part with its callId,
1456+ * name, and state. Skips non-assistant messages and non-tool parts.
1457+ * @internal
1458+ */
1459+ function * iterateToolParts (
1460+ message : UIMessage
1461+ ) : Generator < { part : any ; toolCallId : string ; toolName : string ; state : unknown } > {
1462+ if ( message . role !== "assistant" ) return ;
1463+ for ( const part of ( message . parts ?? [ ] ) as any [ ] ) {
1464+ if ( ! isToolUIPart ( part ) ) continue ;
1465+ const toolCallId = part . toolCallId ;
1466+ if ( typeof toolCallId !== "string" || toolCallId . length === 0 ) continue ;
1467+ yield {
1468+ part,
1469+ toolCallId,
1470+ toolName : getToolName ( part ) ,
1471+ state : part . state ,
1472+ } ;
1473+ }
1474+ }
1475+
1476+ /**
1477+ * Tool parts on the *leaf* assistant message that are still waiting on
1478+ * an answer (`input-available` state). Used to gate fresh user turns
1479+ * during HITL flows.
1480+ * @internal
1481+ */
1482+ function getPendingToolCallsFromHistory ( messages : UIMessage [ ] ) : ChatToolCallRef [ ] {
1483+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
1484+ const msg = messages [ i ] ! ;
1485+ if ( msg . role !== "assistant" ) continue ;
1486+ const pending : ChatToolCallRef [ ] = [ ] ;
1487+ for ( const { toolCallId, toolName, state } of iterateToolParts ( msg ) ) {
1488+ if ( isPendingToolState ( state ) ) {
1489+ pending . push ( { toolCallId, toolName, messageId : msg . id } ) ;
1490+ }
1491+ }
1492+ return pending ;
1493+ }
1494+ return [ ] ;
1495+ }
1496+
1497+ /**
1498+ * All tool parts across the chain that have already produced an output
1499+ * (`output-available` or `output-error`). Used to dedup re-saves when
1500+ * the AI SDK resends an assistant with progressively more answered
1501+ * parts.
1502+ * @internal
1503+ */
1504+ function getResolvedToolCallsFromHistory ( messages : UIMessage [ ] ) : ChatToolCallRef [ ] {
1505+ const out : ChatToolCallRef [ ] = [ ] ;
1506+ for ( const msg of messages ) {
1507+ for ( const { toolCallId, toolName, state } of iterateToolParts ( msg ) ) {
1508+ if ( isResolvedToolState ( state ) ) {
1509+ out . push ( { toolCallId, toolName, messageId : msg . id } ) ;
1510+ }
1511+ }
1512+ }
1513+ return out ;
1514+ }
1515+
1516+ /**
1517+ * Pure helper: tool parts in `message` that have a fresh result not
1518+ * already represented by the resolved toolCallIds in `messages`. The
1519+ * `errorText` field is present only for `output-error` parts.
1520+ * @internal
1521+ */
1522+ function extractNewToolResultsFromHistory (
1523+ message : UIMessage ,
1524+ messages : UIMessage [ ]
1525+ ) : ChatNewToolResult [ ] {
1526+ const resolved = new Set (
1527+ getResolvedToolCallsFromHistory ( messages ) . map ( ( r ) => r . toolCallId )
1528+ ) ;
1529+ const out : ChatNewToolResult [ ] = [ ] ;
1530+ for ( const { part, toolCallId, toolName, state } of iterateToolParts ( message ) ) {
1531+ if ( ! isResolvedToolState ( state ) ) continue ;
1532+ if ( resolved . has ( toolCallId ) ) continue ;
1533+ if ( state === "output-error" ) {
1534+ out . push ( { toolCallId, toolName, output : undefined , errorText : part . errorText } ) ;
1535+ } else {
1536+ out . push ( { toolCallId, toolName, output : part . output } ) ;
1537+ }
1538+ }
1539+ return out ;
1540+ }
1541+
1542+ /**
1543+ * Imperative API for reading and modifying the accumulated message history.
14151544 *
14161545 * Mutations use the same deferred override mechanism as `chat.setMessages()`:
1417- * they are applied at lifecycle checkpoints (after hooks return).
1546+ * they are applied at lifecycle checkpoints (after hooks return). Reads are
1547+ * synchronous against the current accumulator state.
14181548 *
14191549 * Can be called from `onTurnStart`, `onBeforeTurnComplete`, `onTurnComplete`,
1420- * `run()`, or AI SDK tools.
1550+ * `run()`, `onAction`, or AI SDK tools.
14211551 */
14221552const chatHistory = {
14231553 /** Read the current accumulated UI messages (copy). */
14241554 all ( ) : UIMessage [ ] {
14251555 return [ ...getChatHistoryState ( ) ] ;
14261556 } ,
14271557
1558+ /**
1559+ * Read the current chain as an ordered `UIMessage[]`. Alias for `all()` —
1560+ * use this when working alongside parent-aware APIs (TRI-9120) where
1561+ * "chain" disambiguates from "graph".
1562+ */
1563+ getChain ( ) : UIMessage [ ] {
1564+ return [ ...getChatHistoryState ( ) ] ;
1565+ } ,
1566+
1567+ /**
1568+ * Find a message by id. Returns `undefined` if no message with that id
1569+ * is present in the current chain.
1570+ */
1571+ findMessage ( messageId : string ) : UIMessage | undefined {
1572+ return getChatHistoryState ( ) . find ( ( m ) => m . id === messageId ) ;
1573+ } ,
1574+
1575+ /**
1576+ * Tool calls on the leaf assistant message still waiting on an answer
1577+ * (`input-available` state). Use this to gate fresh user turns during
1578+ * HITL flows: if `getPendingToolCalls().length > 0`, an `addToolOutput`
1579+ * is expected before any new user message.
1580+ *
1581+ * Returns `[]` if there is no assistant message yet, or if the leaf
1582+ * assistant has no pending tool calls.
1583+ */
1584+ getPendingToolCalls ( ) : ChatToolCallRef [ ] {
1585+ return getPendingToolCallsFromHistory ( getChatHistoryState ( ) ) ;
1586+ } ,
1587+
1588+ /**
1589+ * Tool calls across the chain with a final result (`output-available`
1590+ * or `output-error`). Use this to dedup re-saves when the AI SDK
1591+ * resends an assistant message with progressively more answered parts.
1592+ */
1593+ getResolvedToolCalls ( ) : ChatToolCallRef [ ] {
1594+ return getResolvedToolCallsFromHistory ( getChatHistoryState ( ) ) ;
1595+ } ,
1596+
1597+ /**
1598+ * Pure helper: returns the tool parts in `message` whose results are
1599+ * not already represented in the current chain. Use this when
1600+ * persisting tool results to your own store: each call surfaces only
1601+ * the *new* answers, so writes stay idempotent across re-streams.
1602+ */
1603+ extractNewToolResults ( message : UIMessage ) : ChatNewToolResult [ ] {
1604+ return extractNewToolResultsFromHistory ( message , getChatHistoryState ( ) ) ;
1605+ } ,
1606+
14281607 /** Replace all accumulated messages. Same as `chat.setMessages()`. */
14291608 set ( messages : UIMessage [ ] ) : void {
14301609 locals . set ( chatOverrideMessagesKey , messages ) ;
0 commit comments