UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

229 lines (228 loc) • 10.4 kB
//#region src/stream/message-reconciliation.ts /** * Merge an authoritative `values.messages` snapshot with the current streamed * message projection. * * Values remain authoritative for ordering and removals. Streamed messages * remain authoritative for in-flight content that has moved past this * snapshot. When the server echoes a same-id message with equal content * plus enrichment (tool calls, `additional_kwargs`, …), values win. * Stream-only messages are preserved until they either appear in values * or are known to have been removed. */ function reconcileMessagesFromValues({ valueMessages, currentMessages, currentIndexById, previousValueMessageIds, streamedMessageIds, preferValuesMessage, addOnly }) { const valueMessageIds = /* @__PURE__ */ new Set(); const merged = []; for (const valuesMessage of valueMessages) { const id = normalizedMessageId(valuesMessage); if (id == null) { merged.push(valuesMessage); continue; } valueMessageIds.add(id); const streamIdx = currentIndexById.get(id); const streamedMessage = streamIdx != null && (streamedMessageIds == null || streamedMessageIds.has(id)) ? currentMessages[streamIdx] : void 0; if (streamedMessage != null && preferValuesMessage?.(valuesMessage, streamedMessage) !== true) merged.push(streamedMessage); else merged.push(valuesMessage); } for (const existing of currentMessages) { const id = normalizedMessageId(existing); if (id == null) continue; if (valueMessageIds.has(id)) continue; if (!addOnly && previousValueMessageIds.has(id)) continue; if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue; merged.push(existing); } return { messages: messagesEqualList(currentMessages, merged) ? currentMessages : merged, valueMessageIds }; } /** * Build a position index for keyed messages. */ function buildMessageIndex(messages) { const index = /* @__PURE__ */ new Map(); messages.forEach((message, idx) => { const id = normalizedMessageId(message); if (id != null) index.set(id, idx); }); return index; } function shouldPreferValuesMessage(valuesMessage, streamedMessage, options) { if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) return true; if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) return false; return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options); } /** Metadata fields that a values echo may enrich on a same-id message. */ const METADATA_ENRICHMENT_KEYS = [ "additional_kwargs", "response_metadata", "usage_metadata" ]; /** * True when values adds or mutates metadata without stripping richer * fields already present on the current copy. * * Covers both key additions (optimistic human gaining * `additional_kwargs.attachments`) and — when * `allowInPlaceMutations` is true — in-place nested mutations of * existing keys (HITL card `status: accepted → done`) as long as every * nested key on the current field is retained. */ function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options) { const allowInPlaceMutations = options?.allowInPlaceMutations !== false; const valuesRecord = valuesMessage; const streamedRecord = streamedMessage; let enriches = false; for (const key of METADATA_ENRICHMENT_KEYS) { const valuesField = valuesRecord[key]; const streamedField = streamedRecord[key]; if (jsonishEqual(valuesField, streamedField)) continue; const valuesEmpty = isEmptyMeta(valuesField); const streamedEmpty = isEmptyMeta(streamedField); if (valuesEmpty && !streamedEmpty) return false; if (!valuesEmpty && streamedEmpty) { enriches = true; continue; } if (allowInPlaceMutations) { if (retainsAllNestedKeys(valuesField, streamedField)) { enriches = true; continue; } return false; } if (isObjectShallowSuperset(valuesField, streamedField)) { enriches = true; continue; } return false; } return enriches; } function isEmptyMeta(value) { if (value == null) return true; if (typeof value === "string") return value.length === 0; if (Array.isArray(value)) return value.length === 0; if (typeof value === "object") return Object.keys(value).length === 0; return false; } /** * True when `candidate` is a shallow object superset of `baseline`: * every baseline key is present with an equal value, and candidate has * at least one extra key. */ function isObjectShallowSuperset(candidate, baseline) { if (candidate == null || baseline == null || typeof candidate !== "object" || typeof baseline !== "object" || Array.isArray(candidate) || Array.isArray(baseline)) return false; const candidateRecord = candidate; const baselineRecord = baseline; const baselineKeys = Object.keys(baselineRecord); if (Object.keys(candidateRecord).length <= baselineKeys.length) return false; for (const key of baselineKeys) { if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) return false; if (!jsonishEqual(candidateRecord[key], baselineRecord[key])) return false; } return true; } /** * True when `candidate` retains every nested key present on `baseline`. * Nested leaf values may differ (in-place mutations); missing keys mean * preferring candidate would erase richer current metadata. */ function retainsAllNestedKeys(candidate, baseline) { if (baseline == null || typeof baseline !== "object") return true; if (candidate == null || typeof candidate !== "object" || Array.isArray(baseline) !== Array.isArray(candidate)) return false; if (Array.isArray(baseline)) { const candidateArr = candidate; const baselineArr = baseline; if (candidateArr.length < baselineArr.length) return false; for (let i = 0; i < baselineArr.length; i += 1) if (!retainsAllNestedKeys(candidateArr[i], baselineArr[i])) return false; return true; } const candidateRecord = candidate; const baselineRecord = baseline; for (const key of Object.keys(baselineRecord)) { if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) return false; if (!retainsAllNestedKeys(candidateRecord[key], baselineRecord[key])) return false; } return true; } /** * Decide whether a values message carries tool-call data missing from the * streamed message. */ function shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage) { const valuesToolCalls = getMessageToolCalls(valuesMessage); if (valuesToolCalls.length === 0) return false; const streamedToolCalls = getMessageToolCalls(streamedMessage); if (streamedToolCalls.length < valuesToolCalls.length) return true; const streamedIds = new Set(streamedToolCalls.map((toolCall) => toolCall.id).filter((id) => typeof id === "string" && id.length > 0)); if (valuesToolCalls.some((toolCall) => { return typeof toolCall.id === "string" && !streamedIds.has(toolCall.id); })) return true; return valuesToolCalls.some((valuesToolCall) => { const streamedToolCall = streamedToolCalls.find((candidate) => typeof valuesToolCall.id === "string" && candidate.id === valuesToolCall.id); return streamedToolCall != null && hasMeaningfulArgs(valuesToolCall.args) && !jsonishEqual(valuesToolCall.args, streamedToolCall.args); }); } function hasMeaningfulArgs(args) { if (args == null) return false; if (typeof args === "string") return args.length > 0; if (typeof args === "object") return Object.keys(args).length > 0; return true; } function messagesEqualList(previous, next) { if (previous === next) return true; if (previous.length !== next.length) return false; for (let i = 0; i < previous.length; i += 1) if (!messagesEqual(previous[i], next[i])) return false; return true; } function messagesEqual(previous, next) { if (previous === next) return true; if (previous == null || next == null) return false; const previousRecord = previous; const nextRecord = next; const previousType = typeof previous.getType === "function" ? previous.getType() : previousRecord.type; const nextType = typeof next.getType === "function" ? next.getType() : nextRecord.type; return previous.id === next.id && previousType === nextType && jsonishEqual(previous.content, next.content) && previousRecord.tool_call_id === nextRecord.tool_call_id && previousRecord.status === nextRecord.status && jsonishEqual(previousRecord.additional_kwargs, nextRecord.additional_kwargs) && jsonishEqual(previousRecord.response_metadata, nextRecord.response_metadata) && jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) && jsonishEqual(previousRecord.tool_call_chunks, nextRecord.tool_call_chunks) && jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata); } function normalizedMessageId(message) { return typeof message.id === "string" && message.id.length > 0 ? message.id : void 0; } function getMessageToolCalls(message) { const raw = message.tool_calls; if (!Array.isArray(raw)) return []; return raw.filter((toolCall) => toolCall != null && typeof toolCall === "object"); } function jsonishEqual(previous, next) { return jsonishEqualAtDepth(previous, next, 0); } function jsonishEqualAtDepth(previous, next, depth) { if (Object.is(previous, next)) return true; if (previous == null || next == null) return false; if (typeof previous !== "object" || typeof next !== "object") return false; if (depth >= 4) return false; if (Array.isArray(previous) || Array.isArray(next)) { if (!Array.isArray(previous) || !Array.isArray(next)) return false; if (previous.length !== next.length) return false; for (let i = 0; i < previous.length; i += 1) if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false; return true; } const previousRecord = previous; const nextRecord = next; const previousKeys = Object.keys(previousRecord).filter((key) => typeof previousRecord[key] !== "function"); const nextKeys = Object.keys(nextRecord).filter((key) => typeof nextRecord[key] !== "function"); if (previousKeys.length !== nextKeys.length) return false; for (const key of previousKeys) { if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false; if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) return false; } return true; } //#endregion exports.buildMessageIndex = buildMessageIndex; exports.messagesEqual = messagesEqual; exports.reconcileMessagesFromValues = reconcileMessagesFromValues; exports.shouldPreferValuesMessage = shouldPreferValuesMessage; //# sourceMappingURL=message-reconciliation.cjs.map