@librechat/agents 3.6.8 → 3.6.10

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.
Files changed (158) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +46 -39
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/agents/projection.cjs +2 -2
  4. package/dist/cjs/agents/projection.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +28 -18
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +87 -16
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/openai/index.cjs +10 -0
  10. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  11. package/dist/cjs/main.cjs +14 -0
  12. package/dist/cjs/messages/alternation.cjs +125 -20
  13. package/dist/cjs/messages/alternation.cjs.map +1 -1
  14. package/dist/cjs/messages/core.cjs +195 -18
  15. package/dist/cjs/messages/core.cjs.map +1 -1
  16. package/dist/cjs/messages/format.cjs +488 -118
  17. package/dist/cjs/messages/format.cjs.map +1 -1
  18. package/dist/cjs/messages/handoffCue.cjs +3 -2
  19. package/dist/cjs/messages/handoffCue.cjs.map +1 -1
  20. package/dist/cjs/messages/index.cjs +1 -0
  21. package/dist/cjs/messages/injected.cjs +5 -2
  22. package/dist/cjs/messages/injected.cjs.map +1 -1
  23. package/dist/cjs/messages/provenance.cjs +411 -0
  24. package/dist/cjs/messages/provenance.cjs.map +1 -0
  25. package/dist/cjs/messages/prune.cjs +18 -13
  26. package/dist/cjs/messages/prune.cjs.map +1 -1
  27. package/dist/cjs/messages/toolResultTypes.cjs +663 -0
  28. package/dist/cjs/messages/toolResultTypes.cjs.map +1 -0
  29. package/dist/cjs/run.cjs +7 -4
  30. package/dist/cjs/run.cjs.map +1 -1
  31. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +21 -4
  32. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  33. package/dist/cjs/tools/CallerCapabilities.cjs +48 -0
  34. package/dist/cjs/tools/CallerCapabilities.cjs.map +1 -0
  35. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs +34 -0
  36. package/dist/cjs/tools/ProgrammaticCallerPolicy.cjs.map +1 -0
  37. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +21 -4
  38. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +25 -24
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +12 -4
  42. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -1
  43. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +13 -4
  44. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs.map +1 -1
  45. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +47 -11
  46. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  47. package/dist/cjs/tools/ptcTimeout.cjs.map +1 -1
  48. package/dist/cjs/tools/search/search.cjs +19 -9
  49. package/dist/cjs/tools/search/search.cjs.map +1 -1
  50. package/dist/cjs/tools/search/tool.cjs +2 -1
  51. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  52. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  53. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  54. package/dist/cjs/utils/toolContent.cjs +2 -2
  55. package/dist/cjs/utils/toolContent.cjs.map +1 -1
  56. package/dist/esm/agents/AgentContext.mjs +46 -39
  57. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  58. package/dist/esm/agents/projection.mjs +2 -2
  59. package/dist/esm/agents/projection.mjs.map +1 -1
  60. package/dist/esm/graphs/Graph.mjs +30 -20
  61. package/dist/esm/graphs/Graph.mjs.map +1 -1
  62. package/dist/esm/graphs/MultiAgentGraph.mjs +87 -16
  63. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  64. package/dist/esm/llm/openai/index.mjs +10 -0
  65. package/dist/esm/llm/openai/index.mjs.map +1 -1
  66. package/dist/esm/main.mjs +4 -3
  67. package/dist/esm/messages/alternation.mjs +125 -20
  68. package/dist/esm/messages/alternation.mjs.map +1 -1
  69. package/dist/esm/messages/core.mjs +196 -19
  70. package/dist/esm/messages/core.mjs.map +1 -1
  71. package/dist/esm/messages/format.mjs +488 -119
  72. package/dist/esm/messages/format.mjs.map +1 -1
  73. package/dist/esm/messages/handoffCue.mjs +3 -2
  74. package/dist/esm/messages/handoffCue.mjs.map +1 -1
  75. package/dist/esm/messages/index.mjs +1 -0
  76. package/dist/esm/messages/injected.mjs +5 -2
  77. package/dist/esm/messages/injected.mjs.map +1 -1
  78. package/dist/esm/messages/provenance.mjs +401 -0
  79. package/dist/esm/messages/provenance.mjs.map +1 -0
  80. package/dist/esm/messages/prune.mjs +18 -13
  81. package/dist/esm/messages/prune.mjs.map +1 -1
  82. package/dist/esm/messages/toolResultTypes.mjs +657 -0
  83. package/dist/esm/messages/toolResultTypes.mjs.map +1 -0
  84. package/dist/esm/run.mjs +7 -4
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +21 -4
  87. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  88. package/dist/esm/tools/CallerCapabilities.mjs +45 -0
  89. package/dist/esm/tools/CallerCapabilities.mjs.map +1 -0
  90. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs +32 -0
  91. package/dist/esm/tools/ProgrammaticCallerPolicy.mjs.map +1 -0
  92. package/dist/esm/tools/ProgrammaticToolCalling.mjs +21 -4
  93. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  94. package/dist/esm/tools/ToolNode.mjs +25 -24
  95. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  96. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +14 -6
  97. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -1
  98. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +15 -6
  99. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs.map +1 -1
  100. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +47 -12
  101. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  102. package/dist/esm/tools/ptcTimeout.mjs.map +1 -1
  103. package/dist/esm/tools/search/search.mjs +19 -9
  104. package/dist/esm/tools/search/search.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +2 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  108. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  109. package/dist/esm/utils/toolContent.mjs +2 -2
  110. package/dist/esm/utils/toolContent.mjs.map +1 -1
  111. package/dist/types/agents/AgentContext.d.ts +8 -13
  112. package/dist/types/agents/projection.d.ts +3 -1
  113. package/dist/types/graphs/Graph.d.ts +1 -1
  114. package/dist/types/llm/openai/index.d.ts +20 -0
  115. package/dist/types/messages/format.d.ts +5 -0
  116. package/dist/types/messages/index.d.ts +1 -0
  117. package/dist/types/messages/provenance.d.ts +85 -0
  118. package/dist/types/messages/toolResultTypes.d.ts +33 -0
  119. package/dist/types/tools/CallerCapabilities.d.ts +12 -0
  120. package/dist/types/tools/ProgrammaticCallerPolicy.d.ts +20 -0
  121. package/dist/types/tools/ToolNode.d.ts +4 -7
  122. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +4 -0
  123. package/dist/types/tools/ptcTimeout.d.ts +8 -0
  124. package/dist/types/tools/search/types.d.ts +15 -0
  125. package/dist/types/types/graph.d.ts +3 -1
  126. package/dist/types/types/tools.d.ts +13 -2
  127. package/package.json +1 -1
  128. package/src/agents/AgentContext.ts +109 -67
  129. package/src/agents/projection.ts +9 -1
  130. package/src/graphs/Graph.ts +56 -23
  131. package/src/graphs/MultiAgentGraph.ts +175 -33
  132. package/src/llm/openai/index.ts +43 -0
  133. package/src/messages/alternation.ts +217 -34
  134. package/src/messages/core.ts +358 -38
  135. package/src/messages/format.ts +958 -137
  136. package/src/messages/handoffCue.ts +7 -4
  137. package/src/messages/index.ts +1 -0
  138. package/src/messages/injected.ts +9 -6
  139. package/src/messages/provenance.ts +897 -0
  140. package/src/messages/prune.ts +28 -10
  141. package/src/messages/toolResultTypes.ts +1174 -0
  142. package/src/run.ts +9 -4
  143. package/src/tools/BashProgrammaticToolCalling.ts +46 -12
  144. package/src/tools/CallerCapabilities.ts +78 -0
  145. package/src/tools/ProgrammaticCallerPolicy.ts +97 -0
  146. package/src/tools/ProgrammaticToolCalling.ts +41 -7
  147. package/src/tools/ToolNode.ts +70 -52
  148. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +24 -26
  149. package/src/tools/local/LocalProgrammaticToolCalling.ts +28 -23
  150. package/src/tools/local/resolveLocalExecutionTools.ts +132 -13
  151. package/src/tools/ptcTimeout.ts +6 -0
  152. package/src/tools/search/search.ts +25 -8
  153. package/src/tools/search/tool.ts +2 -0
  154. package/src/tools/search/types.ts +16 -0
  155. package/src/tools/subagent/SubagentExecutor.ts +8 -4
  156. package/src/types/graph.ts +3 -0
  157. package/src/types/tools.ts +14 -3
  158. package/src/utils/toolContent.ts +2 -2
@@ -17,8 +17,17 @@ import {
17
17
  import type { BaseMessage, AIMessageChunk } from '@langchain/core/messages';
18
18
  import type { LangGraphRunnableConfig } from '@langchain/langgraph';
19
19
  import type { ToolRuntime } from '@langchain/core/tools';
20
+ import type { ProviderMessageProvenancePart } from '@/messages/provenance';
20
21
  import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
21
22
  import type * as t from '@/types';
23
+ import {
24
+ hasBijectiveProviderContentPartMapping,
25
+ inspectProviderMessageProvenance,
26
+ inspectProviderSourceMessageIds,
27
+ setInvalidProviderMessageProvenance,
28
+ setProviderMessageProvenance,
29
+ stampSyntheticProviderMessage,
30
+ } from '@/messages/provenance';
22
31
  import { serializeToolContentBounded } from '@/utils/toolContent';
23
32
  import { Constants, MULTI_AGENT_GRAPH_RUN_NAME } from '@/common';
24
33
  import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
@@ -36,10 +45,12 @@ const HANDOFF_INSTRUCTIONS_KEY = 'handoff_instructions';
36
45
  * message replayed out of the payload.
37
46
  */
38
47
  function buildRoutingPrompt(content: string): HumanMessage {
39
- return new HumanMessage({
40
- content,
41
- additional_kwargs: { role: 'user', isMeta: true, source: 'routing' },
42
- });
48
+ return stampSyntheticProviderMessage(
49
+ new HumanMessage({
50
+ content,
51
+ additional_kwargs: { role: 'user', isMeta: true, source: 'routing' },
52
+ })
53
+ );
43
54
  }
44
55
 
45
56
  function getHandoffInstructions(
@@ -102,23 +113,137 @@ function isTransferToolName(name: unknown): boolean {
102
113
  function filterTransferToolUseBlocks(
103
114
  content: AIMessage['content'],
104
115
  transferToolCallIds: ReadonlySet<string>
105
- ): AIMessage['content'] {
116
+ ): {
117
+ content: AIMessage['content'];
118
+ retainedContentPartIndices?: readonly number[];
119
+ } {
106
120
  if (!Array.isArray(content)) {
107
- return content;
121
+ return { content };
108
122
  }
109
- return content.filter((block) => {
123
+ const filteredContent: typeof content = [];
124
+ const retainedContentPartIndices: number[] = [];
125
+ for (let index = 0; index < content.length; index++) {
126
+ const block = content[index];
110
127
  if (
111
- typeof block !== 'object' ||
112
- (block as { type?: string } | null)?.type !== 'tool_use'
128
+ typeof block === 'object' &&
129
+ (block as { type?: string } | null)?.type === 'tool_use'
113
130
  ) {
114
- return true;
131
+ const toolUse = block as { id?: string; name?: string };
132
+ if (
133
+ (toolUse.id != null && transferToolCallIds.has(toolUse.id)) ||
134
+ isTransferToolName(toolUse.name)
135
+ ) {
136
+ continue;
137
+ }
138
+ }
139
+ filteredContent.push(block);
140
+ retainedContentPartIndices.push(index);
141
+ }
142
+ return { content: filteredContent, retainedContentPartIndices };
143
+ }
144
+
145
+ /** Rebuild a handoff AI message without losing authorship for retained bytes. */
146
+ function copyFilteredAIMessageProvenance(
147
+ source: AIMessage | AIMessageChunk,
148
+ target: AIMessage,
149
+ retainedContentPartIndices?: readonly number[]
150
+ ): AIMessage {
151
+ const sourceMessageIdsState = inspectProviderSourceMessageIds(source);
152
+ const provenanceState = inspectProviderMessageProvenance(source);
153
+ if (
154
+ provenanceState.status === 'invalid' ||
155
+ sourceMessageIdsState.status === 'invalid'
156
+ ) {
157
+ setInvalidProviderMessageProvenance(target);
158
+ return target;
159
+ }
160
+ const sourceMessageIds =
161
+ sourceMessageIdsState.status === 'valid'
162
+ ? sourceMessageIdsState.sourceMessageIds
163
+ : [];
164
+ if (provenanceState.status === 'absent') {
165
+ if (sourceMessageIds.length > 0) {
166
+ setProviderMessageProvenance(
167
+ target,
168
+ sourceMessageIds.map((sourceMessageId) => ({
169
+ attribution: 'model',
170
+ sourceMessageId,
171
+ }))
172
+ );
173
+ }
174
+ return target;
175
+ }
176
+ const provenance = provenanceState.provenance;
177
+ const typedSourceMessageIds = new Set<string>();
178
+ for (const part of provenance.parts) {
179
+ if (part.sourceMessageId != null) {
180
+ typedSourceMessageIds.add(part.sourceMessageId);
181
+ }
182
+ }
183
+ const legacyParts: ProviderMessageProvenancePart[] = [];
184
+ for (const sourceMessageId of sourceMessageIds) {
185
+ if (!typedSourceMessageIds.has(sourceMessageId)) {
186
+ legacyParts.push({ attribution: 'model', sourceMessageId });
187
+ }
188
+ }
189
+ const setFilteredProvenance = (
190
+ parts: readonly ProviderMessageProvenancePart[]
191
+ ): void => {
192
+ if (legacyParts.length === 0) {
193
+ setProviderMessageProvenance(target, parts);
194
+ return;
195
+ }
196
+ const combinedParts = parts.slice();
197
+ for (const part of legacyParts) {
198
+ combinedParts.push(part);
115
199
  }
116
- const toolUse = block as { id?: string; name?: string };
117
- if (toolUse.id != null && transferToolCallIds.has(toolUse.id)) {
118
- return false;
200
+ setProviderMessageProvenance(target, combinedParts);
201
+ };
202
+ if (
203
+ retainedContentPartIndices == null ||
204
+ !Array.isArray(source.content) ||
205
+ retainedContentPartIndices.length === source.content.length
206
+ ) {
207
+ setFilteredProvenance(provenance.parts);
208
+ return target;
209
+ }
210
+
211
+ if (
212
+ !hasBijectiveProviderContentPartMapping(
213
+ provenance.parts,
214
+ source.content.length
215
+ )
216
+ ) {
217
+ /** An older or aggregate envelope cannot be mapped to current blocks.
218
+ * Preserve its conservative attribution instead of dropping tool lineage. */
219
+ setFilteredProvenance(provenance.parts);
220
+ return target;
221
+ }
222
+
223
+ const retainedParts: ProviderMessageProvenancePart[] = [];
224
+ const retainedContentPartIndexSet = new Set(retainedContentPartIndices);
225
+ for (const part of provenance.parts) {
226
+ if (part.sourceContentPartIndices == null) {
227
+ retainedParts.push(part);
228
+ continue;
119
229
  }
120
- return !isTransferToolName(toolUse.name);
121
- });
230
+ const retainedSourceContentPartIndices: number[] = [];
231
+ for (const sourceContentPartIndex of part.sourceContentPartIndices) {
232
+ if (retainedContentPartIndexSet.has(sourceContentPartIndex)) {
233
+ retainedSourceContentPartIndices.push(sourceContentPartIndex);
234
+ }
235
+ }
236
+ if (retainedSourceContentPartIndices.length > 0) {
237
+ retainedParts.push({
238
+ ...part,
239
+ sourceContentPartIndices: retainedSourceContentPartIndices,
240
+ });
241
+ }
242
+ }
243
+ setFilteredProvenance(
244
+ retainedParts.length > 0 ? retainedParts : [{ attribution: 'model' }]
245
+ );
246
+ return target;
122
247
  }
123
248
 
124
249
  function isValidHandoffGroupId(value: unknown): value is number {
@@ -502,15 +627,22 @@ export class MultiAgentGraph extends StandardGraph {
502
627
  const handoffTools: t.GenericTool[] = [];
503
628
  const sourceAgentName = agentContext.name ?? agentId;
504
629
  for (const edge of edges) {
505
- handoffTools.push(
506
- ...this.createHandoffToolsForEdge(edge, agentId, sourceAgentName)
630
+ const edgeTools = this.createHandoffToolsForEdge(
631
+ edge,
632
+ agentId,
633
+ sourceAgentName
507
634
  );
635
+ for (const edgeTool of edgeTools) {
636
+ handoffTools.push(edgeTool);
637
+ }
508
638
  }
509
639
 
510
640
  if (!agentContext.graphTools) {
511
641
  agentContext.graphTools = [];
512
642
  }
513
- agentContext.graphTools.push(...handoffTools);
643
+ for (const handoffTool of handoffTools) {
644
+ agentContext.graphTools.push(handoffTool);
645
+ }
514
646
  }
515
647
  }
516
648
 
@@ -701,11 +833,14 @@ export class MultiAgentGraph extends StandardGraph {
701
833
  * Multiple tool calls - create filtered AIMessage with ONLY this call.
702
834
  * This ensures valid message structure for parallel handoffs.
703
835
  */
704
- const filteredAiMsg = new AIMessage({
705
- content: originalAiMsg.content,
706
- tool_calls: [thisToolCall],
707
- id: originalAiMsg.id,
708
- });
836
+ const filteredAiMsg = copyFilteredAIMessageProvenance(
837
+ originalAiMsg,
838
+ new AIMessage({
839
+ content: originalAiMsg.content,
840
+ tool_calls: [thisToolCall],
841
+ id: originalAiMsg.id,
842
+ })
843
+ );
709
844
 
710
845
  filteredMessages = [
711
846
  ...messages.slice(0, aiMessageIndex),
@@ -981,14 +1116,19 @@ export class MultiAgentGraph extends StandardGraph {
981
1116
  * result in THIS recipient's state, so its id is never
982
1117
  * collected, but its name still marks it.
983
1118
  */
984
- const filteredAiMsg = new AIMessage({
985
- content: filterTransferToolUseBlocks(
986
- aiMsg.content,
987
- transferToolCallIds
988
- ),
989
- tool_calls: remainingToolCalls,
990
- id: aiMsg.id,
991
- });
1119
+ const filteredContent = filterTransferToolUseBlocks(
1120
+ aiMsg.content,
1121
+ transferToolCallIds
1122
+ );
1123
+ const filteredAiMsg = copyFilteredAIMessageProvenance(
1124
+ aiMsg,
1125
+ new AIMessage({
1126
+ content: filteredContent.content,
1127
+ tool_calls: remainingToolCalls,
1128
+ id: aiMsg.id,
1129
+ }),
1130
+ filteredContent.retainedContentPartIndices
1131
+ );
992
1132
  filteredMessages.push(filteredAiMsg);
993
1133
  }
994
1134
  /** If no remaining content or tool calls, skip this message entirely */
@@ -1157,8 +1297,10 @@ export class MultiAgentGraph extends StandardGraph {
1157
1297
  if (lastMsg != null && lastMsg.getType() === 'tool') {
1158
1298
  messagesForAgent = [
1159
1299
  ...filteredMessages,
1160
- new AIMessage(
1161
- `[Processed tool result and transferring to ${agentId}]`
1300
+ stampSyntheticProviderMessage(
1301
+ new AIMessage(
1302
+ `[Processed tool result and transferring to ${agentId}]`
1303
+ )
1162
1304
  ),
1163
1305
  buildRoutingPrompt(instructions),
1164
1306
  ];
@@ -919,6 +919,48 @@ function attachResponsesReplayPosition(
919
919
  chunk.message.lc_kwargs.additional_kwargs = additionalKwargs;
920
920
  }
921
921
 
922
+ type ResponsesAnnotationsBoundaryEvent = {
923
+ type: OpenAIClient.Responses.ResponseStreamEvent['type'];
924
+ response?: {
925
+ output?: Array<{
926
+ type: string;
927
+ content?: Array<{
928
+ type: string;
929
+ annotations?: object[] | null;
930
+ }>;
931
+ }>;
932
+ };
933
+ };
934
+
935
+ /**
936
+ * The Responses API spec declares `annotations` required on `output_text`
937
+ * content parts, but some OpenAI-compatible gateways omit the field on the
938
+ * terminal `response.completed`/`response.incomplete` events. LangChain's
939
+ * converter calls `part.annotations.map(...)` unconditionally, so a missing
940
+ * field crashes the whole stream. Default it to `[]` before conversion.
941
+ */
942
+ export function ensureResponsesOutputAnnotations(
943
+ event: ResponsesAnnotationsBoundaryEvent
944
+ ): void {
945
+ if (event.type !== 'response.completed' && event.type !== 'response.incomplete') {
946
+ return;
947
+ }
948
+ const output = event.response?.output;
949
+ if (!Array.isArray(output)) {
950
+ return;
951
+ }
952
+ for (const item of output) {
953
+ if (item.type !== 'message' || !Array.isArray(item.content)) {
954
+ continue;
955
+ }
956
+ for (const part of item.content) {
957
+ if (part.type === 'output_text' && !Array.isArray(part.annotations)) {
958
+ part.annotations = [];
959
+ }
960
+ }
961
+ }
962
+ }
963
+
922
964
  function getResponsesStreamError(
923
965
  event: OpenAIClient.Responses.ResponseStreamEvent
924
966
  ): Error | undefined {
@@ -960,6 +1002,7 @@ async function* convertLibreChatResponsesStream(
960
1002
  if (streamError != null) {
961
1003
  throw streamError;
962
1004
  }
1005
+ ensureResponsesOutputAnnotations(event);
963
1006
  const convertedChunk =
964
1007
  convertResponsesDeltaToChatGenerationChunk(event) ??
965
1008
  convertDroppedResponsesReplayOutput(event);
@@ -1,6 +1,22 @@
1
1
  // src/messages/alternation.ts
2
2
  import { HumanMessage } from '@langchain/core/messages';
3
3
  import type { BaseMessage, MessageContent } from '@langchain/core/messages';
4
+ import type { ProviderMessageProvenancePart } from './provenance';
5
+ import type { ProviderToolCallIndex } from './toolResultTypes';
6
+ import {
7
+ appendProviderToolCallDescriptor,
8
+ consumeProviderToolResultPair,
9
+ getBoundedProviderPairingArrayProperty,
10
+ getProviderAIMessageToolCallDescriptor,
11
+ getProviderToolCallPartDescriptor,
12
+ getProviderToolResultPartDescriptor,
13
+ } from './toolResultTypes';
14
+ import {
15
+ inspectProviderMessageProvenance,
16
+ inspectProviderSourceMessageIds,
17
+ setInvalidProviderMessageProvenance,
18
+ setProviderMessageProvenance,
19
+ } from './provenance';
4
20
  import { Providers } from '@/common';
5
21
 
6
22
  /**
@@ -19,22 +35,57 @@ export const strictAlternationProviders: ReadonlySet<Providers> = new Set([
19
35
  Providers.MISTRALAI,
20
36
  ]);
21
37
 
22
- const TOOL_RESULT_TYPES = new Set(['tool_result', 'toolResult']);
23
-
24
38
  /**
25
39
  * True when every block is a tool result. Both vendored converters already
26
40
  * merge adjacent runs of these, and folding one into a text turn would break
27
41
  * the tool pairing they depend on — so they are left alone here.
28
42
  */
29
- function isToolResultMessage(message: BaseMessage): boolean {
30
- const { content } = message;
31
- if (typeof content === 'string' || content.length === 0) {
32
- return false;
43
+ function collectProviderToolCalls(message: BaseMessage): ProviderToolCallIndex {
44
+ const calls: ProviderToolCallIndex = new Map();
45
+ const content = getBoundedProviderPairingArrayProperty(message, 'content');
46
+ if (content != null) {
47
+ for (let index = 0; index < content.length; index++) {
48
+ const descriptor = getProviderToolCallPartDescriptor(content[index]);
49
+ if (descriptor != null) {
50
+ appendProviderToolCallDescriptor(calls, descriptor);
51
+ }
52
+ }
33
53
  }
34
- return content.every(
35
- (block) =>
36
- typeof block.type === 'string' && TOOL_RESULT_TYPES.has(block.type)
54
+ const toolCalls = getBoundedProviderPairingArrayProperty(
55
+ message,
56
+ 'tool_calls'
37
57
  );
58
+ if (toolCalls != null) {
59
+ for (let index = 0; index < toolCalls.length; index++) {
60
+ const descriptor = getProviderAIMessageToolCallDescriptor(
61
+ toolCalls[index]
62
+ );
63
+ if (descriptor != null) {
64
+ appendProviderToolCallDescriptor(calls, descriptor);
65
+ }
66
+ }
67
+ }
68
+ return calls;
69
+ }
70
+
71
+ function isToolResultMessage(
72
+ message: BaseMessage,
73
+ pairedToolCalls: ProviderToolCallIndex
74
+ ): boolean {
75
+ const content = getBoundedProviderPairingArrayProperty(message, 'content');
76
+ if (content == null || content.length === 0) {
77
+ return false;
78
+ }
79
+ for (let index = 0; index < content.length; index++) {
80
+ const descriptor = getProviderToolResultPartDescriptor(content[index]);
81
+ if (
82
+ descriptor?.allowHumanMessagePairing !== true ||
83
+ !consumeProviderToolResultPair(descriptor, pairedToolCalls)
84
+ ) {
85
+ return false;
86
+ }
87
+ }
88
+ return true;
38
89
  }
39
90
 
40
91
  function toBlocks(content: MessageContent): Exclude<MessageContent, string> {
@@ -44,14 +95,97 @@ function toBlocks(content: MessageContent): Exclude<MessageContent, string> {
44
95
  return content;
45
96
  }
46
97
 
47
- function joinContent(
48
- left: MessageContent,
49
- right: MessageContent
50
- ): MessageContent {
51
- if (typeof left === 'string' && typeof right === 'string') {
52
- return left === '' ? right : `${left}\n\n${right}`;
98
+ function joinStringContents(
99
+ messages: readonly BaseMessage[],
100
+ endIndex = messages.length
101
+ ): string {
102
+ const contents: string[] = [];
103
+ for (let index = 0; index < endIndex; index++) {
104
+ const content = messages[index].content as string;
105
+ if (contents.length === 0 && content === '') {
106
+ continue;
107
+ }
108
+ contents.push(content);
109
+ }
110
+ return contents.join('\n\n');
111
+ }
112
+
113
+ function joinContents(messages: readonly BaseMessage[]): MessageContent {
114
+ const firstArrayIndex = messages.findIndex((message) =>
115
+ Array.isArray(message.content)
116
+ );
117
+ if (firstArrayIndex === -1) {
118
+ return joinStringContents(messages);
119
+ }
120
+
121
+ const prefix = joinStringContents(messages, firstArrayIndex);
122
+ const blocks: Exclude<MessageContent, string> = [];
123
+ const appendBlocks = (content: MessageContent): void => {
124
+ const contentBlocks = toBlocks(content);
125
+ for (let index = 0; index < contentBlocks.length; index++) {
126
+ blocks.push(contentBlocks[index]);
127
+ }
128
+ };
129
+ appendBlocks(prefix);
130
+ for (let index = firstArrayIndex; index < messages.length; index++) {
131
+ appendBlocks(messages[index].content);
53
132
  }
54
- return [...toBlocks(left), ...toBlocks(right)];
133
+ return blocks;
134
+ }
135
+
136
+ function getHumanMessageProvenanceParts(
137
+ message: BaseMessage
138
+ ): ProviderMessageProvenancePart[] | null {
139
+ const provenanceState = inspectProviderMessageProvenance(message);
140
+ const sourceMessageIdsState = inspectProviderSourceMessageIds(message);
141
+ if (
142
+ provenanceState.status === 'invalid' ||
143
+ sourceMessageIdsState.status === 'invalid'
144
+ ) {
145
+ return null;
146
+ }
147
+ const sourceMessageIds =
148
+ sourceMessageIdsState.status === 'valid'
149
+ ? sourceMessageIdsState.sourceMessageIds
150
+ : [];
151
+ if (provenanceState.status === 'valid') {
152
+ const parts = [...provenanceState.provenance.parts];
153
+ const representedSourceIds = new Set<string>();
154
+ for (const part of parts) {
155
+ if (part.sourceMessageId != null) {
156
+ representedSourceIds.add(part.sourceMessageId);
157
+ }
158
+ }
159
+ const missingSourceIds = sourceMessageIds.filter(
160
+ (sourceMessageId) => !representedSourceIds.has(sourceMessageId)
161
+ );
162
+ if (
163
+ parts.length === 1 &&
164
+ parts[0].sourceMessageId == null &&
165
+ missingSourceIds.length === 1
166
+ ) {
167
+ return missingSourceIds.map((sourceMessageId) => ({
168
+ ...parts[0],
169
+ sourceMessageId,
170
+ }));
171
+ }
172
+ for (const sourceMessageId of missingSourceIds) {
173
+ parts.push({ attribution: 'user', sourceMessageId });
174
+ }
175
+ return parts;
176
+ }
177
+ return sourceMessageIds.length > 0
178
+ ? sourceMessageIds.map((sourceMessageId) => ({
179
+ attribution: 'user' as const,
180
+ sourceMessageId,
181
+ }))
182
+ : [{ attribution: 'user' }];
183
+ }
184
+
185
+ function hasProviderVisibleContent(message: BaseMessage): boolean {
186
+ return typeof message.content === 'string'
187
+ ? message.content.length > 0
188
+ : message.content.length > 0;
55
189
  }
56
190
 
57
191
  /**
@@ -66,25 +200,66 @@ function joinContent(
66
200
  export function coalesceAdjacentUserTurns(
67
201
  messages: BaseMessage[]
68
202
  ): BaseMessage[] {
69
- const result: BaseMessage[] = [];
70
- let mergedAny = false;
71
- for (const message of messages) {
72
- const previous = result[result.length - 1];
73
- const mergeable =
74
- result.length > 0 &&
75
- previous.getType() === 'human' &&
76
- message.getType() === 'human' &&
77
- !isToolResultMessage(previous) &&
78
- !isToolResultMessage(message);
203
+ let result: BaseMessage[] | null = null;
204
+ let pairedToolCalls: ProviderToolCallIndex = new Map();
205
+ let index = 0;
206
+ while (index < messages.length) {
207
+ const first = messages[index];
208
+ if (first.getType() === 'ai') {
209
+ pairedToolCalls = collectProviderToolCalls(first);
210
+ result?.push(first);
211
+ index++;
212
+ continue;
213
+ }
214
+ if (first.getType() !== 'human') {
215
+ result?.push(first);
216
+ index++;
217
+ continue;
218
+ }
219
+ if (isToolResultMessage(first, pairedToolCalls)) {
220
+ result?.push(first);
221
+ index++;
222
+ continue;
223
+ }
79
224
 
80
- if (!mergeable) {
81
- result.push(message);
225
+ let endIndex = index + 1;
226
+ while (
227
+ endIndex < messages.length &&
228
+ messages[endIndex].getType() === 'human' &&
229
+ !isToolResultMessage(messages[endIndex], pairedToolCalls)
230
+ ) {
231
+ endIndex++;
232
+ }
233
+ if (endIndex === index + 1) {
234
+ result?.push(first);
235
+ pairedToolCalls = new Map();
236
+ index = endIndex;
82
237
  continue;
83
238
  }
84
- mergedAny = true;
239
+ result ??= messages.slice(0, index);
240
+ const run = messages.slice(index, endIndex);
241
+ const last = run[run.length - 1];
242
+ const provenanceParts: ProviderMessageProvenancePart[] = [];
243
+ let hasInvalidProvenance = false;
244
+ for (const message of run) {
245
+ if (!hasProviderVisibleContent(message)) {
246
+ continue;
247
+ }
248
+ const messageProvenanceParts = getHumanMessageProvenanceParts(message);
249
+ if (messageProvenanceParts == null) {
250
+ hasInvalidProvenance = true;
251
+ continue;
252
+ }
253
+ for (const part of messageProvenanceParts) {
254
+ provenanceParts.push(part);
255
+ }
256
+ }
257
+ if (provenanceParts.length === 0) {
258
+ provenanceParts.push({ attribution: 'user' });
259
+ }
85
260
 
86
- result[result.length - 1] = new HumanMessage({
87
- content: joinContent(previous.content, message.content),
261
+ const mergedMessage = new HumanMessage({
262
+ content: joinContents(run),
88
263
  /**
89
264
  * The LATER turn's kwargs, deliberately. The one provider-path consumer
90
265
  * of these flags is the prompt-cache tail anchor, and it reasons
@@ -96,9 +271,17 @@ export function coalesceAdjacentUserTurns(
96
271
  * skill body must not pin the cache to the volatile body. The first
97
272
  * turn's id is kept so origin tracking can re-attach by key.
98
273
  */
99
- additional_kwargs: message.additional_kwargs,
100
- ...(previous.id != null && { id: previous.id }),
274
+ additional_kwargs: { ...last.additional_kwargs },
275
+ ...(first.id != null && { id: first.id }),
101
276
  });
277
+ if (hasInvalidProvenance) {
278
+ setInvalidProviderMessageProvenance(mergedMessage);
279
+ } else {
280
+ setProviderMessageProvenance(mergedMessage, provenanceParts);
281
+ }
282
+ result.push(mergedMessage);
283
+ pairedToolCalls = new Map();
284
+ index = endIndex;
102
285
  }
103
286
  /**
104
287
  * Identity on the no-merge path. The pass runs twice for a primary
@@ -108,5 +291,5 @@ export function coalesceAdjacentUserTurns(
108
291
  * the SAME array rather than reallocating a context-sized copy, and
109
292
  * callers can cheaply detect "nothing changed" by identity.
110
293
  */
111
- return mergedAny ? result : messages;
294
+ return result ?? messages;
112
295
  }