UNPKG

@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;" />

345 lines (313 loc) • 9.43 kB
import { dataToUUID, parseJson } from "@copilotkit/shared"; export enum RenderStatus { InProgress = "inProgress", Complete = "complete", } export enum ClaimAction { Create = "create", Override = "override", Existing = "existing", Block = "block", } export interface StateRenderContext { agentId: string; stateRenderId: string; messageId: string; runId: string; messageIndex?: number; } export interface Claim { stateRenderId: string; runId?: string; stateSnapshot?: any; locked?: boolean; messageIndex?: number; } export type ClaimsByMessageId = Record<string, Claim>; export interface ClaimResolution { canRender: boolean; action: ClaimAction; nextClaim?: Claim; lockOthers?: boolean; updateRunId?: string; } export interface SnapshotCaches { byStateRenderAndRun: Record<string, any>; byMessageId: Record<string, any>; } export interface SnapshotSelectionInput { messageId: string; messageName?: string; allowLiveState?: boolean; skipLatestCache?: boolean; stateRenderId?: string; effectiveRunId: string; stateSnapshotProp?: any; agentState?: any; agentMessages?: Array<{ id: string; role?: string }>; existingClaim?: Claim; caches: SnapshotCaches; } export interface SnapshotSelectionResult { snapshot?: any; hasSnapshotKeys: boolean; cachedSnapshot?: any; allowEmptySnapshot?: boolean; snapshotForClaim?: any; } function getStateWithoutConstantKeys(state: any) { if (!state) return {}; const { messages, tools, copilotkit, ...stateWithoutConstantKeys } = state; return stateWithoutConstantKeys; } // Function that compares states, without the constant keys export function areStatesEquals(a: any, b: any) { if ((a && !b) || (!a && b)) return false; const { messages, tools, copilotkit, ...aWithoutConstantKeys } = a; const { messages: bMessages, tools: bTools, copilotkit: bCopilotkit, ...bWithoutConstantKeys } = b; return ( JSON.stringify(aWithoutConstantKeys) === JSON.stringify(bWithoutConstantKeys) ); } export function isPlaceholderMessageId(messageId: string | undefined) { return !!messageId && messageId.startsWith("coagent-state-render-"); } export function isPlaceholderMessageName(messageName: string | undefined) { return messageName === "coagent-state-render"; } export function readCachedMessageEntry(entry: any): { snapshot?: any; runId?: string; } { if (!entry || typeof entry !== "object") { return { snapshot: entry, runId: undefined }; } const snapshot = "snapshot" in entry ? entry.snapshot : entry; const runId = "runId" in entry ? entry.runId : undefined; return { snapshot, runId }; } export function getEffectiveRunId({ existingClaimRunId, cachedMessageRunId, runId, }: { existingClaimRunId?: string; cachedMessageRunId?: string; runId?: string; }) { return existingClaimRunId || cachedMessageRunId || runId || "pending"; } /** * Resolve whether a message can claim a render slot. * This is a pure decision function; the caller applies claim mutations. */ export function resolveClaim({ claims, context, stateSnapshot, }: { claims: ClaimsByMessageId; context: StateRenderContext; stateSnapshot?: any; }): ClaimResolution { const { messageId, stateRenderId, runId, messageIndex } = context; const existing = claims[messageId]; if (existing) { const canRender = existing.stateRenderId === stateRenderId; const shouldUpdateRunId = canRender && runId && (!existing.runId || existing.runId === "pending"); return { canRender, action: canRender ? ClaimAction.Existing : ClaimAction.Block, updateRunId: shouldUpdateRunId ? runId : undefined, }; } const normalizedRunId = runId ?? "pending"; const renderClaimedByOtherMessageEntry = Object.entries(claims).find( ([, claim]) => claim.stateRenderId === stateRenderId && (claim.runId ?? "pending") === normalizedRunId && dataToUUID(getStateWithoutConstantKeys(claim.stateSnapshot)) === dataToUUID(getStateWithoutConstantKeys(stateSnapshot)), ); const renderClaimedByOtherMessage = renderClaimedByOtherMessageEntry?.[1]; const claimedMessageId = renderClaimedByOtherMessageEntry?.[0]; if (renderClaimedByOtherMessage) { if ( messageIndex !== undefined && renderClaimedByOtherMessage.messageIndex !== undefined && messageIndex > renderClaimedByOtherMessage.messageIndex ) { return { canRender: true, action: ClaimAction.Override, nextClaim: { stateRenderId, runId, messageIndex }, lockOthers: runId === renderClaimedByOtherMessage.runId || isPlaceholderMessageId(claimedMessageId), }; } if ( runId && renderClaimedByOtherMessage.runId && runId !== renderClaimedByOtherMessage.runId ) { return { canRender: true, action: ClaimAction.Override, nextClaim: { stateRenderId, runId, messageIndex }, lockOthers: isPlaceholderMessageId(claimedMessageId), }; } if (isPlaceholderMessageId(claimedMessageId)) { return { canRender: true, action: ClaimAction.Override, nextClaim: { stateRenderId, runId, messageIndex }, lockOthers: true, }; } if ( stateSnapshot && renderClaimedByOtherMessage.stateSnapshot && !areStatesEquals(renderClaimedByOtherMessage.stateSnapshot, stateSnapshot) ) { return { canRender: true, action: ClaimAction.Override, nextClaim: { stateRenderId, runId }, }; } return { canRender: false, action: ClaimAction.Block }; } if (!runId) { return { canRender: false, action: ClaimAction.Block }; } return { canRender: true, action: ClaimAction.Create, nextClaim: { stateRenderId, runId, messageIndex }, }; } /** * Select the best snapshot to render for this message. * Priority order is: * 1) explicit message snapshot * 2) live agent state (latest assistant only) * 3) cached snapshot for message * 4) cached snapshot for stateRenderId+runId * 5) last cached snapshot for stateRenderId */ export function selectSnapshot({ messageId, messageName, allowLiveState, skipLatestCache, stateRenderId, effectiveRunId, stateSnapshotProp, agentState, agentMessages, existingClaim, caches, }: SnapshotSelectionInput): SnapshotSelectionResult { const lastAssistantId = agentMessages ? [...agentMessages].reverse().find((msg) => msg.role === "assistant")?.id : undefined; const latestSnapshot = stateRenderId !== undefined ? caches.byStateRenderAndRun[`${stateRenderId}::latest`] : undefined; const messageIndex = agentMessages ? agentMessages.findIndex((msg) => msg.id === messageId) : -1; const messageRole = messageIndex >= 0 && agentMessages ? agentMessages[messageIndex]?.role : undefined; let previousUserMessageId: string | undefined; if (messageIndex > 0 && agentMessages) { for (let i = messageIndex - 1; i >= 0; i -= 1) { if (agentMessages[i]?.role === "user") { previousUserMessageId = agentMessages[i]?.id; break; } } } const liveStateIsStale = stateSnapshotProp === undefined && latestSnapshot !== undefined && agentState !== undefined && areStatesEquals(latestSnapshot, agentState); const shouldUseLiveState = (Boolean(allowLiveState) || !lastAssistantId || messageId === lastAssistantId) && !liveStateIsStale; const snapshot = stateSnapshotProp ? parseJson(stateSnapshotProp, stateSnapshotProp) : shouldUseLiveState ? agentState : undefined; const hasSnapshotKeys = !!(snapshot && Object.keys(snapshot).length > 0); const allowEmptySnapshot = snapshot !== undefined && !hasSnapshotKeys && (stateSnapshotProp !== undefined || shouldUseLiveState); const messageCacheEntry = caches.byMessageId[messageId]; const cachedMessageSnapshot = readCachedMessageEntry(messageCacheEntry).snapshot; const cacheKey = stateRenderId !== undefined ? `${stateRenderId}::${effectiveRunId}` : undefined; let cachedSnapshot = cachedMessageSnapshot ?? caches.byMessageId[messageId]; if ( cachedSnapshot === undefined && cacheKey && caches.byStateRenderAndRun[cacheKey] !== undefined ) { cachedSnapshot = caches.byStateRenderAndRun[cacheKey]; } if ( cachedSnapshot === undefined && stateRenderId && previousUserMessageId && caches.byStateRenderAndRun[ `${stateRenderId}::pending:${previousUserMessageId}` ] !== undefined ) { cachedSnapshot = caches.byStateRenderAndRun[ `${stateRenderId}::pending:${previousUserMessageId}` ]; } if ( cachedSnapshot === undefined && !skipLatestCache && stateRenderId && messageRole !== "assistant" && (stateSnapshotProp !== undefined || (agentState && Object.keys(agentState).length > 0)) ) { cachedSnapshot = caches.byStateRenderAndRun[`${stateRenderId}::latest`]; } const snapshotForClaim = existingClaim?.locked ? (existingClaim.stateSnapshot ?? cachedSnapshot) : hasSnapshotKeys ? snapshot : (existingClaim?.stateSnapshot ?? cachedSnapshot); return { snapshot, hasSnapshotKeys, cachedSnapshot, allowEmptySnapshot, snapshotForClaim, }; }