@langchain/langgraph-sdk 1.9.31 → 1.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/stream/transport/http.cjs +2 -0
- package/dist/client/stream/transport/http.cjs.map +1 -1
- package/dist/client/stream/transport/http.d.cts.map +1 -1
- package/dist/client/stream/transport/http.d.ts.map +1 -1
- package/dist/client/stream/transport/http.js +2 -0
- package/dist/client/stream/transport/http.js.map +1 -1
- package/dist/stream/controller.cjs +95 -23
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.d.cts.map +1 -1
- package/dist/stream/controller.d.ts.map +1 -1
- package/dist/stream/controller.js +95 -23
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/message-reconciliation.cjs +47 -22
- package/dist/stream/message-reconciliation.cjs.map +1 -1
- package/dist/stream/message-reconciliation.js +47 -22
- package/dist/stream/message-reconciliation.js.map +1 -1
- package/dist/stream/root-message-projection.cjs +7 -5
- package/dist/stream/root-message-projection.cjs.map +1 -1
- package/dist/stream/root-message-projection.js +7 -5
- package/dist/stream/root-message-projection.js.map +1 -1
- package/package.json +6 -16
|
@@ -49,27 +49,10 @@ function buildMessageIndex(messages) {
|
|
|
49
49
|
});
|
|
50
50
|
return index;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
* Decide whether a values message should replace a same-id streamed or
|
|
54
|
-
* optimistic message.
|
|
55
|
-
*
|
|
56
|
-
* Values win when they carry data the current copy lacks:
|
|
57
|
-
* - finalized tool-call args (see
|
|
58
|
-
* {@link shouldPreferValuesMessageForToolCalls})
|
|
59
|
-
* - metadata enrichment with unchanged content (e.g. server-committed
|
|
60
|
-
* `additional_kwargs.attachments` on an optimistic human)
|
|
61
|
-
*
|
|
62
|
-
* Preferring is asymmetric: a lagging/poorer values snapshot must not
|
|
63
|
-
* replace a richer current copy (seeded `getState()` metadata, messages-
|
|
64
|
-
* channel `usage_metadata`, etc.) merely because the two messages differ.
|
|
65
|
-
*
|
|
66
|
-
* Streamed content still wins when it has moved past this snapshot, so
|
|
67
|
-
* in-flight token streaming is not overwritten by a lagging values event.
|
|
68
|
-
*/
|
|
69
|
-
function shouldPreferValuesMessage(valuesMessage, streamedMessage) {
|
|
52
|
+
function shouldPreferValuesMessage(valuesMessage, streamedMessage, options) {
|
|
70
53
|
if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) return true;
|
|
71
54
|
if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) return false;
|
|
72
|
-
return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage);
|
|
55
|
+
return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options);
|
|
73
56
|
}
|
|
74
57
|
/** Metadata fields that a values echo may enrich on a same-id message. */
|
|
75
58
|
const METADATA_ENRICHMENT_KEYS = [
|
|
@@ -78,10 +61,17 @@ const METADATA_ENRICHMENT_KEYS = [
|
|
|
78
61
|
"usage_metadata"
|
|
79
62
|
];
|
|
80
63
|
/**
|
|
81
|
-
* True when values adds
|
|
82
|
-
*
|
|
64
|
+
* True when values adds or mutates metadata without stripping richer
|
|
65
|
+
* fields already present on the current copy.
|
|
66
|
+
*
|
|
67
|
+
* Covers both key additions (optimistic human gaining
|
|
68
|
+
* `additional_kwargs.attachments`) and — when
|
|
69
|
+
* `allowInPlaceMutations` is true — in-place nested mutations of
|
|
70
|
+
* existing keys (HITL card `status: accepted → done`) as long as every
|
|
71
|
+
* nested key on the current field is retained.
|
|
83
72
|
*/
|
|
84
|
-
function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage) {
|
|
73
|
+
function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options) {
|
|
74
|
+
const allowInPlaceMutations = options?.allowInPlaceMutations !== false;
|
|
85
75
|
const valuesRecord = valuesMessage;
|
|
86
76
|
const streamedRecord = streamedMessage;
|
|
87
77
|
let enriches = false;
|
|
@@ -96,6 +86,13 @@ function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage) {
|
|
|
96
86
|
enriches = true;
|
|
97
87
|
continue;
|
|
98
88
|
}
|
|
89
|
+
if (allowInPlaceMutations) {
|
|
90
|
+
if (retainsAllNestedKeys(valuesField, streamedField)) {
|
|
91
|
+
enriches = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
99
96
|
if (isObjectShallowSuperset(valuesField, streamedField)) {
|
|
100
97
|
enriches = true;
|
|
101
98
|
continue;
|
|
@@ -111,6 +108,11 @@ function isEmptyMeta(value) {
|
|
|
111
108
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
112
109
|
return false;
|
|
113
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* True when `candidate` is a shallow object superset of `baseline`:
|
|
113
|
+
* every baseline key is present with an equal value, and candidate has
|
|
114
|
+
* at least one extra key.
|
|
115
|
+
*/
|
|
114
116
|
function isObjectShallowSuperset(candidate, baseline) {
|
|
115
117
|
if (candidate == null || baseline == null || typeof candidate !== "object" || typeof baseline !== "object" || Array.isArray(candidate) || Array.isArray(baseline)) return false;
|
|
116
118
|
const candidateRecord = candidate;
|
|
@@ -124,6 +126,29 @@ function isObjectShallowSuperset(candidate, baseline) {
|
|
|
124
126
|
return true;
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
129
|
+
* True when `candidate` retains every nested key present on `baseline`.
|
|
130
|
+
* Nested leaf values may differ (in-place mutations); missing keys mean
|
|
131
|
+
* preferring candidate would erase richer current metadata.
|
|
132
|
+
*/
|
|
133
|
+
function retainsAllNestedKeys(candidate, baseline) {
|
|
134
|
+
if (baseline == null || typeof baseline !== "object") return true;
|
|
135
|
+
if (candidate == null || typeof candidate !== "object" || Array.isArray(baseline) !== Array.isArray(candidate)) return false;
|
|
136
|
+
if (Array.isArray(baseline)) {
|
|
137
|
+
const candidateArr = candidate;
|
|
138
|
+
const baselineArr = baseline;
|
|
139
|
+
if (candidateArr.length < baselineArr.length) return false;
|
|
140
|
+
for (let i = 0; i < baselineArr.length; i += 1) if (!retainsAllNestedKeys(candidateArr[i], baselineArr[i])) return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const candidateRecord = candidate;
|
|
144
|
+
const baselineRecord = baseline;
|
|
145
|
+
for (const key of Object.keys(baselineRecord)) {
|
|
146
|
+
if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) return false;
|
|
147
|
+
if (!retainsAllNestedKeys(candidateRecord[key], baselineRecord[key])) return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
127
152
|
* Decide whether a values message carries tool-call data missing from the
|
|
128
153
|
* streamed message.
|
|
129
154
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries data the current copy lacks (finalized tool calls, or\n * same-content metadata such as committed `additional_kwargs`).\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content that has moved past this\n * snapshot. When the server echoes a same-id message with equal content\n * plus enrichment (tool calls, `additional_kwargs`, …), values win.\n * Stream-only messages are preserved until they either appear in values\n * or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message should replace a same-id streamed or\n * optimistic message.\n *\n * Values win when they carry data the current copy lacks:\n * - finalized tool-call args (see\n * {@link shouldPreferValuesMessageForToolCalls})\n * - metadata enrichment with unchanged content (e.g. server-committed\n * `additional_kwargs.attachments` on an optimistic human)\n *\n * Preferring is asymmetric: a lagging/poorer values snapshot must not\n * replace a richer current copy (seeded `getState()` metadata, messages-\n * channel `usage_metadata`, etc.) merely because the two messages differ.\n *\n * Streamed content still wins when it has moved past this snapshot, so\n * in-flight token streaming is not overwritten by a lagging values event.\n */\nexport function shouldPreferValuesMessage(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) {\n return true;\n }\n if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) {\n return false;\n }\n return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage);\n}\n\n/** Metadata fields that a values echo may enrich on a same-id message. */\nconst METADATA_ENRICHMENT_KEYS = [\n \"additional_kwargs\",\n \"response_metadata\",\n \"usage_metadata\",\n] as const;\n\n/**\n * True when values adds metadata the current copy lacks, without\n * stripping richer fields already present on the current copy.\n */\nfunction valuesAddsMetadataEnrichment(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesRecord = valuesMessage as unknown as Record<string, unknown>;\n const streamedRecord = streamedMessage as unknown as Record<string, unknown>;\n\n let enriches = false;\n for (const key of METADATA_ENRICHMENT_KEYS) {\n const valuesField = valuesRecord[key];\n const streamedField = streamedRecord[key];\n if (jsonishEqual(valuesField, streamedField)) continue;\n\n const valuesEmpty = isEmptyMeta(valuesField);\n const streamedEmpty = isEmptyMeta(streamedField);\n\n if (valuesEmpty && !streamedEmpty) {\n // Preferring values would erase richer current metadata.\n return false;\n }\n if (!valuesEmpty && streamedEmpty) {\n enriches = true;\n continue;\n }\n // Both non-empty and differ: only treat as enrichment when values\n // is a shallow object superset of the current field (adds keys,\n // keeps existing ones). Conflicting shapes keep the current copy.\n if (isObjectShallowSuperset(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n return enriches;\n}\n\nfunction isEmptyMeta(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") {\n return Object.keys(value as Record<string, unknown>).length === 0;\n }\n return false;\n}\n\nfunction isObjectShallowSuperset(\n candidate: unknown,\n baseline: unknown\n): boolean {\n if (\n candidate == null ||\n baseline == null ||\n typeof candidate !== \"object\" ||\n typeof baseline !== \"object\" ||\n Array.isArray(candidate) ||\n Array.isArray(baseline)\n ) {\n return false;\n }\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n const baselineKeys = Object.keys(baselineRecord);\n const candidateKeys = Object.keys(candidateRecord);\n if (candidateKeys.length <= baselineKeys.length) return false;\n for (const key of baselineKeys) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!jsonishEqual(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-reconciliation.cjs","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries data the current copy lacks (finalized tool calls, or\n * same-content metadata such as committed `additional_kwargs`).\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content that has moved past this\n * snapshot. When the server echoes a same-id message with equal content\n * plus enrichment (tool calls, `additional_kwargs`, …), values win.\n * Stream-only messages are preserved until they either appear in values\n * or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message should replace a same-id streamed or\n * optimistic message.\n *\n * Values win when they carry data the current copy lacks:\n * - finalized tool-call args (see\n * {@link shouldPreferValuesMessageForToolCalls})\n * - metadata enrichment or in-place nested metadata mutations with\n * unchanged content (e.g. server-committed `additional_kwargs`, or\n * `response_metadata.card.status` updating from `accepted` → `done`)\n *\n * Preferring is asymmetric: a lagging/poorer values snapshot must not\n * replace a richer current copy (seeded `getState()` metadata, messages-\n * channel `usage_metadata`, etc.) merely because the two messages differ.\n * Values that would drop nested metadata keys keep the current copy.\n *\n * In-place leaf mutations are only applied when\n * {@link ShouldPreferValuesMessageOptions.allowInPlaceMutations} is true\n * (the default). Callers pass `false` for stale/`addOnly` reconnect\n * snapshots so an older `accepted` card cannot overwrite a newer `done`.\n *\n * Streamed content still wins when it has moved past this snapshot, so\n * in-flight token streaming is not overwritten by a lagging values event.\n */\nexport interface ShouldPreferValuesMessageOptions {\n /**\n * When false, only prefer values for key-adding enrichment (or filling\n * empty metadata), not for same-key nested leaf mutations. Used for\n * lagging reconnect/`addOnly` snapshots.\n *\n * @default true\n */\n readonly allowInPlaceMutations?: boolean;\n}\n\nexport function shouldPreferValuesMessage(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage,\n options?: ShouldPreferValuesMessageOptions\n): boolean {\n if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) {\n return true;\n }\n if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) {\n return false;\n }\n return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options);\n}\n\n/** Metadata fields that a values echo may enrich on a same-id message. */\nconst METADATA_ENRICHMENT_KEYS = [\n \"additional_kwargs\",\n \"response_metadata\",\n \"usage_metadata\",\n] as const;\n\n/**\n * True when values adds or mutates metadata without stripping richer\n * fields already present on the current copy.\n *\n * Covers both key additions (optimistic human gaining\n * `additional_kwargs.attachments`) and — when\n * `allowInPlaceMutations` is true — in-place nested mutations of\n * existing keys (HITL card `status: accepted → done`) as long as every\n * nested key on the current field is retained.\n */\nfunction valuesAddsMetadataEnrichment(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage,\n options?: ShouldPreferValuesMessageOptions\n): boolean {\n const allowInPlaceMutations = options?.allowInPlaceMutations !== false;\n const valuesRecord = valuesMessage as unknown as Record<string, unknown>;\n const streamedRecord = streamedMessage as unknown as Record<string, unknown>;\n\n let enriches = false;\n for (const key of METADATA_ENRICHMENT_KEYS) {\n const valuesField = valuesRecord[key];\n const streamedField = streamedRecord[key];\n if (jsonishEqual(valuesField, streamedField)) continue;\n\n const valuesEmpty = isEmptyMeta(valuesField);\n const streamedEmpty = isEmptyMeta(streamedField);\n\n if (valuesEmpty && !streamedEmpty) {\n // Preferring values would erase richer current metadata.\n return false;\n }\n if (!valuesEmpty && streamedEmpty) {\n enriches = true;\n continue;\n }\n // Both non-empty and differ.\n if (allowInPlaceMutations) {\n // Prefer values when it retains every nested key (leaf mutations\n // and added keys allowed). Dropping keys would erase richer\n // current metadata — keep the current copy.\n if (retainsAllNestedKeys(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n // Stale/addOnly snapshots: only treat as enrichment when values is\n // a shallow key-superset with equal existing values. Same-key leaf\n // mutations must not roll current metadata backward.\n if (isObjectShallowSuperset(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n return enriches;\n}\n\nfunction isEmptyMeta(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") {\n return Object.keys(value as Record<string, unknown>).length === 0;\n }\n return false;\n}\n\n/**\n * True when `candidate` is a shallow object superset of `baseline`:\n * every baseline key is present with an equal value, and candidate has\n * at least one extra key.\n */\nfunction isObjectShallowSuperset(\n candidate: unknown,\n baseline: unknown\n): boolean {\n if (\n candidate == null ||\n baseline == null ||\n typeof candidate !== \"object\" ||\n typeof baseline !== \"object\" ||\n Array.isArray(candidate) ||\n Array.isArray(baseline)\n ) {\n return false;\n }\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n const baselineKeys = Object.keys(baselineRecord);\n const candidateKeys = Object.keys(candidateRecord);\n if (candidateKeys.length <= baselineKeys.length) return false;\n for (const key of baselineKeys) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!jsonishEqual(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * True when `candidate` retains every nested key present on `baseline`.\n * Nested leaf values may differ (in-place mutations); missing keys mean\n * preferring candidate would erase richer current metadata.\n */\nfunction retainsAllNestedKeys(candidate: unknown, baseline: unknown): boolean {\n if (baseline == null || typeof baseline !== \"object\") {\n // Leaf baseline — candidate may replace the value in place.\n return true;\n }\n if (\n candidate == null ||\n typeof candidate !== \"object\" ||\n Array.isArray(baseline) !== Array.isArray(candidate)\n ) {\n return false;\n }\n\n if (Array.isArray(baseline)) {\n const candidateArr = candidate as unknown[];\n const baselineArr = baseline as unknown[];\n if (candidateArr.length < baselineArr.length) return false;\n for (let i = 0; i < baselineArr.length; i += 1) {\n if (!retainsAllNestedKeys(candidateArr[i], baselineArr[i])) {\n return false;\n }\n }\n return true;\n }\n\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n for (const key of Object.keys(baselineRecord)) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!retainsAllNestedKeys(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -49,27 +49,10 @@ function buildMessageIndex(messages) {
|
|
|
49
49
|
});
|
|
50
50
|
return index;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
* Decide whether a values message should replace a same-id streamed or
|
|
54
|
-
* optimistic message.
|
|
55
|
-
*
|
|
56
|
-
* Values win when they carry data the current copy lacks:
|
|
57
|
-
* - finalized tool-call args (see
|
|
58
|
-
* {@link shouldPreferValuesMessageForToolCalls})
|
|
59
|
-
* - metadata enrichment with unchanged content (e.g. server-committed
|
|
60
|
-
* `additional_kwargs.attachments` on an optimistic human)
|
|
61
|
-
*
|
|
62
|
-
* Preferring is asymmetric: a lagging/poorer values snapshot must not
|
|
63
|
-
* replace a richer current copy (seeded `getState()` metadata, messages-
|
|
64
|
-
* channel `usage_metadata`, etc.) merely because the two messages differ.
|
|
65
|
-
*
|
|
66
|
-
* Streamed content still wins when it has moved past this snapshot, so
|
|
67
|
-
* in-flight token streaming is not overwritten by a lagging values event.
|
|
68
|
-
*/
|
|
69
|
-
function shouldPreferValuesMessage(valuesMessage, streamedMessage) {
|
|
52
|
+
function shouldPreferValuesMessage(valuesMessage, streamedMessage, options) {
|
|
70
53
|
if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) return true;
|
|
71
54
|
if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) return false;
|
|
72
|
-
return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage);
|
|
55
|
+
return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options);
|
|
73
56
|
}
|
|
74
57
|
/** Metadata fields that a values echo may enrich on a same-id message. */
|
|
75
58
|
const METADATA_ENRICHMENT_KEYS = [
|
|
@@ -78,10 +61,17 @@ const METADATA_ENRICHMENT_KEYS = [
|
|
|
78
61
|
"usage_metadata"
|
|
79
62
|
];
|
|
80
63
|
/**
|
|
81
|
-
* True when values adds
|
|
82
|
-
*
|
|
64
|
+
* True when values adds or mutates metadata without stripping richer
|
|
65
|
+
* fields already present on the current copy.
|
|
66
|
+
*
|
|
67
|
+
* Covers both key additions (optimistic human gaining
|
|
68
|
+
* `additional_kwargs.attachments`) and — when
|
|
69
|
+
* `allowInPlaceMutations` is true — in-place nested mutations of
|
|
70
|
+
* existing keys (HITL card `status: accepted → done`) as long as every
|
|
71
|
+
* nested key on the current field is retained.
|
|
83
72
|
*/
|
|
84
|
-
function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage) {
|
|
73
|
+
function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options) {
|
|
74
|
+
const allowInPlaceMutations = options?.allowInPlaceMutations !== false;
|
|
85
75
|
const valuesRecord = valuesMessage;
|
|
86
76
|
const streamedRecord = streamedMessage;
|
|
87
77
|
let enriches = false;
|
|
@@ -96,6 +86,13 @@ function valuesAddsMetadataEnrichment(valuesMessage, streamedMessage) {
|
|
|
96
86
|
enriches = true;
|
|
97
87
|
continue;
|
|
98
88
|
}
|
|
89
|
+
if (allowInPlaceMutations) {
|
|
90
|
+
if (retainsAllNestedKeys(valuesField, streamedField)) {
|
|
91
|
+
enriches = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
99
96
|
if (isObjectShallowSuperset(valuesField, streamedField)) {
|
|
100
97
|
enriches = true;
|
|
101
98
|
continue;
|
|
@@ -111,6 +108,11 @@ function isEmptyMeta(value) {
|
|
|
111
108
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
112
109
|
return false;
|
|
113
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* True when `candidate` is a shallow object superset of `baseline`:
|
|
113
|
+
* every baseline key is present with an equal value, and candidate has
|
|
114
|
+
* at least one extra key.
|
|
115
|
+
*/
|
|
114
116
|
function isObjectShallowSuperset(candidate, baseline) {
|
|
115
117
|
if (candidate == null || baseline == null || typeof candidate !== "object" || typeof baseline !== "object" || Array.isArray(candidate) || Array.isArray(baseline)) return false;
|
|
116
118
|
const candidateRecord = candidate;
|
|
@@ -124,6 +126,29 @@ function isObjectShallowSuperset(candidate, baseline) {
|
|
|
124
126
|
return true;
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
129
|
+
* True when `candidate` retains every nested key present on `baseline`.
|
|
130
|
+
* Nested leaf values may differ (in-place mutations); missing keys mean
|
|
131
|
+
* preferring candidate would erase richer current metadata.
|
|
132
|
+
*/
|
|
133
|
+
function retainsAllNestedKeys(candidate, baseline) {
|
|
134
|
+
if (baseline == null || typeof baseline !== "object") return true;
|
|
135
|
+
if (candidate == null || typeof candidate !== "object" || Array.isArray(baseline) !== Array.isArray(candidate)) return false;
|
|
136
|
+
if (Array.isArray(baseline)) {
|
|
137
|
+
const candidateArr = candidate;
|
|
138
|
+
const baselineArr = baseline;
|
|
139
|
+
if (candidateArr.length < baselineArr.length) return false;
|
|
140
|
+
for (let i = 0; i < baselineArr.length; i += 1) if (!retainsAllNestedKeys(candidateArr[i], baselineArr[i])) return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const candidateRecord = candidate;
|
|
144
|
+
const baselineRecord = baseline;
|
|
145
|
+
for (const key of Object.keys(baselineRecord)) {
|
|
146
|
+
if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) return false;
|
|
147
|
+
if (!retainsAllNestedKeys(candidateRecord[key], baselineRecord[key])) return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
127
152
|
* Decide whether a values message carries tool-call data missing from the
|
|
128
153
|
* streamed message.
|
|
129
154
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries data the current copy lacks (finalized tool calls, or\n * same-content metadata such as committed `additional_kwargs`).\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content that has moved past this\n * snapshot. When the server echoes a same-id message with equal content\n * plus enrichment (tool calls, `additional_kwargs`, …), values win.\n * Stream-only messages are preserved until they either appear in values\n * or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message should replace a same-id streamed or\n * optimistic message.\n *\n * Values win when they carry data the current copy lacks:\n * - finalized tool-call args (see\n * {@link shouldPreferValuesMessageForToolCalls})\n * - metadata enrichment with unchanged content (e.g. server-committed\n * `additional_kwargs.attachments` on an optimistic human)\n *\n * Preferring is asymmetric: a lagging/poorer values snapshot must not\n * replace a richer current copy (seeded `getState()` metadata, messages-\n * channel `usage_metadata`, etc.) merely because the two messages differ.\n *\n * Streamed content still wins when it has moved past this snapshot, so\n * in-flight token streaming is not overwritten by a lagging values event.\n */\nexport function shouldPreferValuesMessage(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) {\n return true;\n }\n if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) {\n return false;\n }\n return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage);\n}\n\n/** Metadata fields that a values echo may enrich on a same-id message. */\nconst METADATA_ENRICHMENT_KEYS = [\n \"additional_kwargs\",\n \"response_metadata\",\n \"usage_metadata\",\n] as const;\n\n/**\n * True when values adds metadata the current copy lacks, without\n * stripping richer fields already present on the current copy.\n */\nfunction valuesAddsMetadataEnrichment(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesRecord = valuesMessage as unknown as Record<string, unknown>;\n const streamedRecord = streamedMessage as unknown as Record<string, unknown>;\n\n let enriches = false;\n for (const key of METADATA_ENRICHMENT_KEYS) {\n const valuesField = valuesRecord[key];\n const streamedField = streamedRecord[key];\n if (jsonishEqual(valuesField, streamedField)) continue;\n\n const valuesEmpty = isEmptyMeta(valuesField);\n const streamedEmpty = isEmptyMeta(streamedField);\n\n if (valuesEmpty && !streamedEmpty) {\n // Preferring values would erase richer current metadata.\n return false;\n }\n if (!valuesEmpty && streamedEmpty) {\n enriches = true;\n continue;\n }\n // Both non-empty and differ: only treat as enrichment when values\n // is a shallow object superset of the current field (adds keys,\n // keeps existing ones). Conflicting shapes keep the current copy.\n if (isObjectShallowSuperset(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n return enriches;\n}\n\nfunction isEmptyMeta(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") {\n return Object.keys(value as Record<string, unknown>).length === 0;\n }\n return false;\n}\n\nfunction isObjectShallowSuperset(\n candidate: unknown,\n baseline: unknown\n): boolean {\n if (\n candidate == null ||\n baseline == null ||\n typeof candidate !== \"object\" ||\n typeof baseline !== \"object\" ||\n Array.isArray(candidate) ||\n Array.isArray(baseline)\n ) {\n return false;\n }\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n const baselineKeys = Object.keys(baselineRecord);\n const candidateKeys = Object.keys(candidateRecord);\n if (candidateKeys.length <= baselineKeys.length) return false;\n for (const key of baselineKeys) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!jsonishEqual(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-reconciliation.js","names":[],"sources":["../../src/stream/message-reconciliation.ts"],"sourcesContent":["import type { BaseMessage } from \"@langchain/core/messages\";\n\nexport interface ReconcileMessagesFromValuesOptions {\n /**\n * Messages from the authoritative `values.messages` snapshot.\n */\n readonly valueMessages: readonly BaseMessage[];\n /**\n * Current message projection, including stream-assembled in-flight messages.\n */\n readonly currentMessages: readonly BaseMessage[];\n /**\n * Index from message id to current message position.\n */\n readonly currentIndexById: ReadonlyMap<string, number>;\n /**\n * Ids observed in the most recent previous `values.messages` snapshot.\n * If one of these ids is missing from the next snapshot, it is treated as\n * an explicit server-side removal.\n */\n readonly previousValueMessageIds: ReadonlySet<string>;\n /**\n * Optional stream-id filter. When supplied, only these current ids are\n * eligible to override the values snapshot. When omitted, any id present in\n * `currentIndexById` is eligible, preserving the root controller's historic\n * behavior.\n */\n readonly streamedMessageIds?: ReadonlySet<string>;\n /**\n * Allows callers to keep a values message even when a streamed message with\n * the same id exists. Used by the root controller when the values message\n * carries data the current copy lacks (finalized tool calls, or\n * same-content metadata such as committed `additional_kwargs`).\n */\n readonly preferValuesMessage?: (\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n ) => boolean;\n /**\n * When true, treat the snapshot as a non-authoritative (older / replayed)\n * view: never drop a current message just because it is absent from this\n * snapshot. Used on reconnect, where the content pump replays older\n * checkpoints after the authoritative `getState()` seed — an older\n * snapshot legitimately lacks later messages and must not remove them.\n */\n readonly addOnly?: boolean;\n}\n\nexport interface ReconciledMessages {\n readonly messages: readonly BaseMessage[];\n readonly valueMessageIds: Set<string>;\n}\n\n/**\n * Merge an authoritative `values.messages` snapshot with the current streamed\n * message projection.\n *\n * Values remain authoritative for ordering and removals. Streamed messages\n * remain authoritative for in-flight content that has moved past this\n * snapshot. When the server echoes a same-id message with equal content\n * plus enrichment (tool calls, `additional_kwargs`, …), values win.\n * Stream-only messages are preserved until they either appear in values\n * or are known to have been removed.\n */\nexport function reconcileMessagesFromValues({\n valueMessages,\n currentMessages,\n currentIndexById,\n previousValueMessageIds,\n streamedMessageIds,\n preferValuesMessage,\n addOnly,\n}: ReconcileMessagesFromValuesOptions): ReconciledMessages {\n const valueMessageIds = new Set<string>();\n const merged: BaseMessage[] = [];\n\n for (const valuesMessage of valueMessages) {\n const id = normalizedMessageId(valuesMessage);\n if (id == null) {\n merged.push(valuesMessage);\n continue;\n }\n\n valueMessageIds.add(id);\n const streamIdx = currentIndexById.get(id);\n const canUseStreamed =\n streamIdx != null &&\n (streamedMessageIds == null || streamedMessageIds.has(id));\n const streamedMessage = canUseStreamed\n ? currentMessages[streamIdx]\n : undefined;\n\n if (\n streamedMessage != null &&\n preferValuesMessage?.(valuesMessage, streamedMessage) !== true\n ) {\n merged.push(streamedMessage);\n } else {\n merged.push(valuesMessage);\n }\n }\n\n for (const existing of currentMessages) {\n const id = normalizedMessageId(existing);\n if (id == null) continue;\n if (valueMessageIds.has(id)) continue;\n // A previously-seen id missing from this snapshot is a server-side\n // removal — UNLESS this is an older/replayed snapshot (`addOnly`),\n // where the absence only means \"this earlier checkpoint predates the\n // message\", not \"the message was removed\".\n if (!addOnly && previousValueMessageIds.has(id)) continue;\n if (streamedMessageIds != null && !streamedMessageIds.has(id)) continue;\n merged.push(existing);\n }\n\n return {\n messages: messagesEqualList(currentMessages, merged)\n ? currentMessages\n : merged,\n valueMessageIds,\n };\n}\n\n/**\n * Build a position index for keyed messages.\n */\nexport function buildMessageIndex(\n messages: readonly BaseMessage[]\n): Map<string, number> {\n const index = new Map<string, number>();\n messages.forEach((message, idx) => {\n const id = normalizedMessageId(message);\n if (id != null) index.set(id, idx);\n });\n return index;\n}\n\n/**\n * Decide whether a values message should replace a same-id streamed or\n * optimistic message.\n *\n * Values win when they carry data the current copy lacks:\n * - finalized tool-call args (see\n * {@link shouldPreferValuesMessageForToolCalls})\n * - metadata enrichment or in-place nested metadata mutations with\n * unchanged content (e.g. server-committed `additional_kwargs`, or\n * `response_metadata.card.status` updating from `accepted` → `done`)\n *\n * Preferring is asymmetric: a lagging/poorer values snapshot must not\n * replace a richer current copy (seeded `getState()` metadata, messages-\n * channel `usage_metadata`, etc.) merely because the two messages differ.\n * Values that would drop nested metadata keys keep the current copy.\n *\n * In-place leaf mutations are only applied when\n * {@link ShouldPreferValuesMessageOptions.allowInPlaceMutations} is true\n * (the default). Callers pass `false` for stale/`addOnly` reconnect\n * snapshots so an older `accepted` card cannot overwrite a newer `done`.\n *\n * Streamed content still wins when it has moved past this snapshot, so\n * in-flight token streaming is not overwritten by a lagging values event.\n */\nexport interface ShouldPreferValuesMessageOptions {\n /**\n * When false, only prefer values for key-adding enrichment (or filling\n * empty metadata), not for same-key nested leaf mutations. Used for\n * lagging reconnect/`addOnly` snapshots.\n *\n * @default true\n */\n readonly allowInPlaceMutations?: boolean;\n}\n\nexport function shouldPreferValuesMessage(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage,\n options?: ShouldPreferValuesMessageOptions\n): boolean {\n if (shouldPreferValuesMessageForToolCalls(valuesMessage, streamedMessage)) {\n return true;\n }\n if (!jsonishEqual(valuesMessage.content, streamedMessage.content)) {\n return false;\n }\n return valuesAddsMetadataEnrichment(valuesMessage, streamedMessage, options);\n}\n\n/** Metadata fields that a values echo may enrich on a same-id message. */\nconst METADATA_ENRICHMENT_KEYS = [\n \"additional_kwargs\",\n \"response_metadata\",\n \"usage_metadata\",\n] as const;\n\n/**\n * True when values adds or mutates metadata without stripping richer\n * fields already present on the current copy.\n *\n * Covers both key additions (optimistic human gaining\n * `additional_kwargs.attachments`) and — when\n * `allowInPlaceMutations` is true — in-place nested mutations of\n * existing keys (HITL card `status: accepted → done`) as long as every\n * nested key on the current field is retained.\n */\nfunction valuesAddsMetadataEnrichment(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage,\n options?: ShouldPreferValuesMessageOptions\n): boolean {\n const allowInPlaceMutations = options?.allowInPlaceMutations !== false;\n const valuesRecord = valuesMessage as unknown as Record<string, unknown>;\n const streamedRecord = streamedMessage as unknown as Record<string, unknown>;\n\n let enriches = false;\n for (const key of METADATA_ENRICHMENT_KEYS) {\n const valuesField = valuesRecord[key];\n const streamedField = streamedRecord[key];\n if (jsonishEqual(valuesField, streamedField)) continue;\n\n const valuesEmpty = isEmptyMeta(valuesField);\n const streamedEmpty = isEmptyMeta(streamedField);\n\n if (valuesEmpty && !streamedEmpty) {\n // Preferring values would erase richer current metadata.\n return false;\n }\n if (!valuesEmpty && streamedEmpty) {\n enriches = true;\n continue;\n }\n // Both non-empty and differ.\n if (allowInPlaceMutations) {\n // Prefer values when it retains every nested key (leaf mutations\n // and added keys allowed). Dropping keys would erase richer\n // current metadata — keep the current copy.\n if (retainsAllNestedKeys(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n // Stale/addOnly snapshots: only treat as enrichment when values is\n // a shallow key-superset with equal existing values. Same-key leaf\n // mutations must not roll current metadata backward.\n if (isObjectShallowSuperset(valuesField, streamedField)) {\n enriches = true;\n continue;\n }\n return false;\n }\n return enriches;\n}\n\nfunction isEmptyMeta(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (typeof value === \"object\") {\n return Object.keys(value as Record<string, unknown>).length === 0;\n }\n return false;\n}\n\n/**\n * True when `candidate` is a shallow object superset of `baseline`:\n * every baseline key is present with an equal value, and candidate has\n * at least one extra key.\n */\nfunction isObjectShallowSuperset(\n candidate: unknown,\n baseline: unknown\n): boolean {\n if (\n candidate == null ||\n baseline == null ||\n typeof candidate !== \"object\" ||\n typeof baseline !== \"object\" ||\n Array.isArray(candidate) ||\n Array.isArray(baseline)\n ) {\n return false;\n }\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n const baselineKeys = Object.keys(baselineRecord);\n const candidateKeys = Object.keys(candidateRecord);\n if (candidateKeys.length <= baselineKeys.length) return false;\n for (const key of baselineKeys) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!jsonishEqual(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * True when `candidate` retains every nested key present on `baseline`.\n * Nested leaf values may differ (in-place mutations); missing keys mean\n * preferring candidate would erase richer current metadata.\n */\nfunction retainsAllNestedKeys(candidate: unknown, baseline: unknown): boolean {\n if (baseline == null || typeof baseline !== \"object\") {\n // Leaf baseline — candidate may replace the value in place.\n return true;\n }\n if (\n candidate == null ||\n typeof candidate !== \"object\" ||\n Array.isArray(baseline) !== Array.isArray(candidate)\n ) {\n return false;\n }\n\n if (Array.isArray(baseline)) {\n const candidateArr = candidate as unknown[];\n const baselineArr = baseline as unknown[];\n if (candidateArr.length < baselineArr.length) return false;\n for (let i = 0; i < baselineArr.length; i += 1) {\n if (!retainsAllNestedKeys(candidateArr[i], baselineArr[i])) {\n return false;\n }\n }\n return true;\n }\n\n const candidateRecord = candidate as Record<string, unknown>;\n const baselineRecord = baseline as Record<string, unknown>;\n for (const key of Object.keys(baselineRecord)) {\n if (!Object.prototype.hasOwnProperty.call(candidateRecord, key)) {\n return false;\n }\n if (!retainsAllNestedKeys(candidateRecord[key], baselineRecord[key])) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Decide whether a values message carries tool-call data missing from the\n * streamed message.\n */\nexport function shouldPreferValuesMessageForToolCalls(\n valuesMessage: BaseMessage,\n streamedMessage: BaseMessage\n): boolean {\n const valuesToolCalls = getMessageToolCalls(valuesMessage);\n if (valuesToolCalls.length === 0) return false;\n\n const streamedToolCalls = getMessageToolCalls(streamedMessage);\n if (streamedToolCalls.length < valuesToolCalls.length) return true;\n\n const streamedIds = new Set(\n streamedToolCalls\n .map((toolCall) => toolCall.id)\n .filter((id): id is string => typeof id === \"string\" && id.length > 0)\n );\n if (\n valuesToolCalls.some((toolCall) => {\n return typeof toolCall.id === \"string\" && !streamedIds.has(toolCall.id);\n })\n ) {\n return true;\n }\n\n // Values snapshots carry the finalized tool-call args. Prefer them only when\n // they add meaningful data, so empty placeholder args do not replace an\n // otherwise useful streamed message.\n return valuesToolCalls.some((valuesToolCall) => {\n const streamedToolCall = streamedToolCalls.find(\n (candidate) =>\n typeof valuesToolCall.id === \"string\" &&\n candidate.id === valuesToolCall.id\n );\n return (\n streamedToolCall != null &&\n hasMeaningfulArgs(valuesToolCall.args) &&\n !jsonishEqual(valuesToolCall.args, streamedToolCall.args)\n );\n });\n}\n\nfunction hasMeaningfulArgs(args: unknown): boolean {\n if (args == null) return false;\n if (typeof args === \"string\") return args.length > 0;\n if (typeof args === \"object\") return Object.keys(args).length > 0;\n return true;\n}\n\nexport function messagesEqualList(\n previous: readonly BaseMessage[],\n next: readonly BaseMessage[]\n): boolean {\n if (previous === next) return true;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!messagesEqual(previous[i], next[i])) return false;\n }\n return true;\n}\n\nexport function messagesEqual(\n previous: BaseMessage | undefined,\n next: BaseMessage | undefined\n): boolean {\n if (previous === next) return true;\n if (previous == null || next == null) return false;\n const previousRecord = previous as unknown as Record<string, unknown>;\n const nextRecord = next as unknown as Record<string, unknown>;\n const previousType =\n typeof previous.getType === \"function\"\n ? previous.getType()\n : previousRecord.type;\n const nextType =\n typeof next.getType === \"function\" ? next.getType() : nextRecord.type;\n\n return (\n previous.id === next.id &&\n previousType === nextType &&\n jsonishEqual(previous.content, next.content) &&\n previousRecord.tool_call_id === nextRecord.tool_call_id &&\n previousRecord.status === nextRecord.status &&\n jsonishEqual(\n previousRecord.additional_kwargs,\n nextRecord.additional_kwargs\n ) &&\n jsonishEqual(\n previousRecord.response_metadata,\n nextRecord.response_metadata\n ) &&\n jsonishEqual(previousRecord.tool_calls, nextRecord.tool_calls) &&\n jsonishEqual(\n previousRecord.tool_call_chunks,\n nextRecord.tool_call_chunks\n ) &&\n jsonishEqual(previousRecord.usage_metadata, nextRecord.usage_metadata)\n );\n}\n\nfunction normalizedMessageId(message: BaseMessage): string | undefined {\n return typeof message.id === \"string\" && message.id.length > 0\n ? message.id\n : undefined;\n}\n\nfunction getMessageToolCalls(\n message: BaseMessage\n): Array<{ id?: string; name?: string; args?: unknown }> {\n const raw = (message as unknown as { tool_calls?: unknown }).tool_calls;\n if (!Array.isArray(raw)) return [];\n return raw.filter(\n (toolCall): toolCall is { id?: string; name?: string; args?: unknown } =>\n toolCall != null && typeof toolCall === \"object\"\n );\n}\n\nfunction jsonishEqual(previous: unknown, next: unknown): boolean {\n return jsonishEqualAtDepth(previous, next, 0);\n}\n\nfunction jsonishEqualAtDepth(\n previous: unknown,\n next: unknown,\n depth: number\n): boolean {\n if (Object.is(previous, next)) return true;\n if (previous == null || next == null) return false;\n if (typeof previous !== \"object\" || typeof next !== \"object\") return false;\n if (depth >= 4) return false;\n\n if (Array.isArray(previous) || Array.isArray(next)) {\n if (!Array.isArray(previous) || !Array.isArray(next)) return false;\n if (previous.length !== next.length) return false;\n for (let i = 0; i < previous.length; i += 1) {\n if (!jsonishEqualAtDepth(previous[i], next[i], depth + 1)) return false;\n }\n return true;\n }\n\n const previousRecord = previous as Record<string, unknown>;\n const nextRecord = next as Record<string, unknown>;\n const previousKeys = Object.keys(previousRecord).filter(\n (key) => typeof previousRecord[key] !== \"function\"\n );\n const nextKeys = Object.keys(nextRecord).filter(\n (key) => typeof nextRecord[key] !== \"function\"\n );\n if (previousKeys.length !== nextKeys.length) return false;\n\n for (const key of previousKeys) {\n if (!Object.prototype.hasOwnProperty.call(nextRecord, key)) return false;\n if (!jsonishEqualAtDepth(previousRecord[key], nextRecord[key], depth + 1)) {\n return false;\n }\n }\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -262,7 +262,7 @@ var RootMessageProjection = class {
|
|
|
262
262
|
currentMessages: baselineMessages,
|
|
263
263
|
currentIndexById: this.#indexById,
|
|
264
264
|
previousValueMessageIds: this.#valuesMessageIds,
|
|
265
|
-
preferValuesMessage: require_message_reconciliation.shouldPreferValuesMessage,
|
|
265
|
+
preferValuesMessage: (valuesMessage, streamedMessage) => require_message_reconciliation.shouldPreferValuesMessage(valuesMessage, streamedMessage, { allowInPlaceMutations: !addOnly }),
|
|
266
266
|
addOnly
|
|
267
267
|
});
|
|
268
268
|
if (!addOnly) this.#valuesMessageIds = reconciliation.valueMessageIds;
|
|
@@ -289,10 +289,12 @@ var RootMessageProjection = class {
|
|
|
289
289
|
* emits a `values` snapshot containing the same ids,
|
|
290
290
|
* {@link applyValues} → {@link reconcileMessagesFromValues} takes over
|
|
291
291
|
* (server ordering wins; the echoed message replaces the optimistic
|
|
292
|
-
* one when content matches *and* values adds
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
292
|
+
* one when content matches *and* values adds or mutates nested
|
|
293
|
+
* metadata without dropping keys the optimistic copy already has, so
|
|
294
|
+
* server-authored `additional_kwargs` / in-place
|
|
295
|
+
* `response_metadata` updates are visible without waiting for
|
|
296
|
+
* hydration — lagging/poorer snapshots do not strip richer current
|
|
297
|
+
* metadata).
|
|
296
298
|
* In-flight streamed content still wins when it has moved past the
|
|
297
299
|
* snapshot.
|
|
298
300
|
*
|