@copilotkit/react-core
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
101 lines (98 loc) • 4.68 kB
TypeScript
import * as React from 'react';
import { ReactCustomMessageRendererPosition } from '@copilotkitnext/react';
/**
* Bridge hook that connects agent state renders to chat messages.
*
* ## Purpose
* This hook finds matching state render configurations (registered via useCoAgentStateRender)
* and returns UI to render in chat.
* It ensures each state render appears bound to a specific message, preventing duplicates while
* allowing re-binding when the underlying state changes significantly.
*
* ## Message-ID-Based Claiming System
*
* ### The Problem
* Multiple bridge component instances render simultaneously (one per message). Without coordination,
* they would all try to render the same state render, causing duplicates.
*
* ### The Solution: Message-ID Claims with State Comparison
* Each state render is "claimed" by exactly one **message ID** (not runId):
*
* **Claim Structure**: `claimsRef.current[messageId] = { stateRenderId, runId, stateSnapshot, locked }`
*
* **Primary binding is by messageId because**:
* - runId is not always available immediately (starts as "pending")
* - messageId is the stable identifier throughout the message lifecycle
* - Claims persist across component remounts via context ref
*
* ### Claiming Logic Flow
*
* 1. **Message already has a claim**:
* - Check if the claim matches the current stateRenderId
* - If yes → render (this message owns this render)
* - Update runId if it was "pending" and now available
*
* 2. **State render claimed by another message**:
* - Compare state snapshots (ignoring constant keys: messages, tools, copilotkit)
* - If states are identical → block rendering (duplicate)
* - **If states are different → allow claiming** (new data, new message)
* - This handles cases where the same render type shows different states in different messages
*
* 3. **Unclaimed state render**:
* - Only allow claiming if runId is "pending" (initial render)
* - If runId is real but no claim exists → block (edge case protection)
* - Create new claim: `claimsRef.current[messageId] = { stateRenderId, runId }`
*
* ### State Snapshot Locking
*
* Once a state snapshot is captured and locked for a message:
* - The UI always renders with the locked snapshot (not live agent.state)
* - Prevents UI from appearing "wiped" during state transitions
* - Locked when: stateSnapshot prop is available (from message persistence)
* - Unlocked state: can still update from live agent.state
*
* ### Synchronous Claiming (Ref-based)
*
* Claims are stored in a context-level ref (not React state):
* - Multiple bridges render in the same tick
* - State updates are async - would allow duplicates before update completes
* - Ref provides immediate, synchronous claim checking
* - Survives component remounts (stored in context, not component)
*
* ## Flow Example
*
* ```
* Time 1: Message A renders, runId=undefined, state={progress: 50%}
* → effectiveRunId = "pending"
* → Claims: claimsRef["msgA"] = { stateRenderId: "tasks", runId: "pending", stateSnapshot: {progress: 50%} }
* → Renders UI with 50% progress
*
* Time 2: Message B renders, runId=undefined, same state
* → Checks: "tasks" already claimed by msgA with same state
* → Returns null (blocked - duplicate)
*
* Time 3: Real runId appears (e.g., "run-123")
* → Updates claim: claimsRef["msgA"].runId = "run-123"
* → Message A continues rendering
*
* Time 4: Agent processes more, state={progress: 100%}
* → Message A: locked to 50% (stateSnapshot locked)
* → Message C renders with state={progress: 100%}
* → Checks: "tasks" claimed by msgA but state is DIFFERENT (50% vs 100%)
* → Allows new claim: claimsRef["msgC"] = { stateRenderId: "tasks", runId: "run-123", stateSnapshot: {progress: 100%} }
* → Both messages render independently with their own snapshots
* ```
*/
interface CoAgentStateRenderBridgeProps {
message: any;
position: ReactCustomMessageRendererPosition;
runId: string;
messageIndex: number;
messageIndexInRun: number;
numberOfMessagesInRun: number;
agentId: string;
stateSnapshot: any;
}
declare function useCoagentStateRenderBridge(agentId: string, props: CoAgentStateRenderBridgeProps): string | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | null | undefined;
declare function CoAgentStateRenderBridge(props: CoAgentStateRenderBridgeProps): string | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | null | undefined;
export { CoAgentStateRenderBridge, CoAgentStateRenderBridgeProps, useCoagentStateRenderBridge };