@librechat/agents 3.1.42 → 3.1.44
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/cjs/agents/AgentContext.cjs +26 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +31 -2
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +80 -10
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +17 -3
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +26 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +31 -2
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +81 -11
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +17 -3
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/types/tools/ToolSearch.d.ts +3 -1
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +28 -1
- package/src/messages/cache.test.ts +82 -1
- package/src/messages/cache.ts +42 -3
- package/src/messages/ensureThinkingBlock.test.ts +207 -0
- package/src/messages/format.ts +106 -10
- package/src/tools/ToolSearch.ts +20 -3
- package/src/tools/__tests__/ToolSearch.test.ts +62 -2
|
@@ -89,6 +89,213 @@ describe('ensureThinkingBlockInMessages', () => {
|
|
|
89
89
|
);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test('should not modify AI message when reasoning_content is not the first block (Bedrock whitespace artifact)', () => {
|
|
93
|
+
// Bedrock emits a "\n\n" text chunk before the thinking block,
|
|
94
|
+
// pushing reasoning_content to content[1] instead of content[0].
|
|
95
|
+
const messages = [
|
|
96
|
+
new HumanMessage({ content: 'Do something' }),
|
|
97
|
+
new AIMessage({
|
|
98
|
+
content: [
|
|
99
|
+
{ type: 'text', text: '\n\n' },
|
|
100
|
+
{
|
|
101
|
+
type: ContentTypes.REASONING_CONTENT,
|
|
102
|
+
reasoningText: { text: 'Let me think about this' },
|
|
103
|
+
},
|
|
104
|
+
{ type: 'text', text: 'Let me help!' },
|
|
105
|
+
],
|
|
106
|
+
tool_calls: [
|
|
107
|
+
{
|
|
108
|
+
id: 'call_bedrock',
|
|
109
|
+
name: 'some_tool',
|
|
110
|
+
args: { x: 1 },
|
|
111
|
+
type: 'tool_call' as const,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
}),
|
|
115
|
+
new ToolMessage({
|
|
116
|
+
content: 'tool result',
|
|
117
|
+
tool_call_id: 'call_bedrock',
|
|
118
|
+
}),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const result = ensureThinkingBlockInMessages(messages, Providers.BEDROCK);
|
|
122
|
+
|
|
123
|
+
expect(result).toHaveLength(3);
|
|
124
|
+
expect(result[0]).toBeInstanceOf(HumanMessage);
|
|
125
|
+
expect(result[1]).toBeInstanceOf(AIMessage);
|
|
126
|
+
expect(result[2]).toBeInstanceOf(ToolMessage);
|
|
127
|
+
// The AI message should be preserved, not converted to a HumanMessage
|
|
128
|
+
expect(result[1].content).toEqual(messages[1].content);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('should not convert follow-up tool calls in a thinking-enabled chain (Bedrock multi-step)', () => {
|
|
132
|
+
// Bedrock reasoning models produce reasoning on the first AI response,
|
|
133
|
+
// then subsequent tool calls in the same chain have content: "" with no
|
|
134
|
+
// reasoning block. These should NOT be converted because the chain
|
|
135
|
+
// already has a thinking block upstream.
|
|
136
|
+
const messages = [
|
|
137
|
+
new HumanMessage({ content: 'show me something cool' }),
|
|
138
|
+
new AIMessage({
|
|
139
|
+
content: [
|
|
140
|
+
{ type: 'text', text: '\n\n' },
|
|
141
|
+
{
|
|
142
|
+
type: ContentTypes.REASONING_CONTENT,
|
|
143
|
+
reasoningText: { text: 'Let me navigate to a page' },
|
|
144
|
+
},
|
|
145
|
+
{ type: 'text', text: 'Let me whip up something fun!' },
|
|
146
|
+
],
|
|
147
|
+
tool_calls: [
|
|
148
|
+
{
|
|
149
|
+
id: 'call_nav',
|
|
150
|
+
name: 'navigate_page',
|
|
151
|
+
args: { url: 'about:blank' },
|
|
152
|
+
type: 'tool_call' as const,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
new ToolMessage({
|
|
157
|
+
content: 'Navigated to about:blank',
|
|
158
|
+
tool_call_id: 'call_nav',
|
|
159
|
+
}),
|
|
160
|
+
// Follow-up: content: "", tool calls, NO reasoning block
|
|
161
|
+
new AIMessage({
|
|
162
|
+
content: '',
|
|
163
|
+
tool_calls: [
|
|
164
|
+
{
|
|
165
|
+
id: 'call_eval',
|
|
166
|
+
name: 'evaluate_script',
|
|
167
|
+
args: { script: 'document.title = "test"' },
|
|
168
|
+
type: 'tool_call' as const,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
}),
|
|
172
|
+
new ToolMessage({
|
|
173
|
+
content: 'Script executed',
|
|
174
|
+
tool_call_id: 'call_eval',
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const result = ensureThinkingBlockInMessages(messages, Providers.BEDROCK);
|
|
179
|
+
|
|
180
|
+
// All 5 messages preserved — the follow-up AI message at index 3 is NOT converted
|
|
181
|
+
expect(result).toHaveLength(5);
|
|
182
|
+
expect(result[0]).toBeInstanceOf(HumanMessage);
|
|
183
|
+
expect(result[1]).toBeInstanceOf(AIMessage);
|
|
184
|
+
expect(result[2]).toBeInstanceOf(ToolMessage);
|
|
185
|
+
expect(result[3]).toBeInstanceOf(AIMessage);
|
|
186
|
+
expect(result[3].content).toBe('');
|
|
187
|
+
expect((result[3] as AIMessage).tool_calls).toHaveLength(1);
|
|
188
|
+
expect(result[4]).toBeInstanceOf(ToolMessage);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('should not convert multiple follow-up tool calls in a long chain', () => {
|
|
192
|
+
// Three AI→Tool rounds: only the first has reasoning
|
|
193
|
+
const messages = [
|
|
194
|
+
new HumanMessage({ content: 'do stuff' }),
|
|
195
|
+
new AIMessage({
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: ContentTypes.REASONING_CONTENT,
|
|
199
|
+
reasoningText: { text: 'Planning...' },
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
tool_calls: [
|
|
203
|
+
{ id: 'c1', name: 'step1', args: {}, type: 'tool_call' as const },
|
|
204
|
+
],
|
|
205
|
+
}),
|
|
206
|
+
new ToolMessage({ content: 'r1', tool_call_id: 'c1' }),
|
|
207
|
+
new AIMessage({
|
|
208
|
+
content: '',
|
|
209
|
+
tool_calls: [
|
|
210
|
+
{ id: 'c2', name: 'step2', args: {}, type: 'tool_call' as const },
|
|
211
|
+
],
|
|
212
|
+
}),
|
|
213
|
+
new ToolMessage({ content: 'r2', tool_call_id: 'c2' }),
|
|
214
|
+
new AIMessage({
|
|
215
|
+
content: '',
|
|
216
|
+
tool_calls: [
|
|
217
|
+
{ id: 'c3', name: 'step3', args: {}, type: 'tool_call' as const },
|
|
218
|
+
],
|
|
219
|
+
}),
|
|
220
|
+
new ToolMessage({ content: 'r3', tool_call_id: 'c3' }),
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
const result = ensureThinkingBlockInMessages(messages, Providers.BEDROCK);
|
|
224
|
+
|
|
225
|
+
expect(result).toHaveLength(7);
|
|
226
|
+
expect(result[1]).toBeInstanceOf(AIMessage);
|
|
227
|
+
expect(result[3]).toBeInstanceOf(AIMessage);
|
|
228
|
+
expect(result[5]).toBeInstanceOf(AIMessage);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should still convert non-thinking agent tool calls after a human message boundary', () => {
|
|
232
|
+
// A chain with thinking, then a new human message, then a chain WITHOUT thinking
|
|
233
|
+
const messages = [
|
|
234
|
+
new HumanMessage({ content: 'first request' }),
|
|
235
|
+
new AIMessage({
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: ContentTypes.REASONING_CONTENT,
|
|
239
|
+
reasoningText: { text: 'Thinking...' },
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
tool_calls: [
|
|
243
|
+
{ id: 'c1', name: 'tool1', args: {}, type: 'tool_call' as const },
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
new ToolMessage({ content: 'r1', tool_call_id: 'c1' }),
|
|
247
|
+
new HumanMessage({ content: 'second request' }),
|
|
248
|
+
// This chain has NO thinking blocks — should be converted
|
|
249
|
+
new AIMessage({
|
|
250
|
+
content: 'Using a tool',
|
|
251
|
+
tool_calls: [
|
|
252
|
+
{ id: 'c2', name: 'tool2', args: {}, type: 'tool_call' as const },
|
|
253
|
+
],
|
|
254
|
+
}),
|
|
255
|
+
new ToolMessage({ content: 'r2', tool_call_id: 'c2' }),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const result = ensureThinkingBlockInMessages(messages, Providers.BEDROCK);
|
|
259
|
+
|
|
260
|
+
// First chain preserved (3 msgs), human preserved, second chain converted (1 HumanMessage)
|
|
261
|
+
expect(result).toHaveLength(5);
|
|
262
|
+
expect(result[0]).toBeInstanceOf(HumanMessage);
|
|
263
|
+
expect(result[1]).toBeInstanceOf(AIMessage); // reasoning chain — kept
|
|
264
|
+
expect(result[2]).toBeInstanceOf(ToolMessage);
|
|
265
|
+
expect(result[3]).toBeInstanceOf(HumanMessage); // user message
|
|
266
|
+
expect(result[4]).toBeInstanceOf(HumanMessage); // converted — no thinking in this chain
|
|
267
|
+
expect(result[4].content).toContain('[Previous agent context]');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('should detect thinking via additional_kwargs.reasoning_content in chain', () => {
|
|
271
|
+
const messages = [
|
|
272
|
+
new HumanMessage({ content: 'hello' }),
|
|
273
|
+
new AIMessage({
|
|
274
|
+
content: '',
|
|
275
|
+
additional_kwargs: {
|
|
276
|
+
reasoning_content: 'Some reasoning...',
|
|
277
|
+
},
|
|
278
|
+
tool_calls: [
|
|
279
|
+
{ id: 'c1', name: 'tool1', args: {}, type: 'tool_call' as const },
|
|
280
|
+
],
|
|
281
|
+
}),
|
|
282
|
+
new ToolMessage({ content: 'r1', tool_call_id: 'c1' }),
|
|
283
|
+
new AIMessage({
|
|
284
|
+
content: '',
|
|
285
|
+
tool_calls: [
|
|
286
|
+
{ id: 'c2', name: 'tool2', args: {}, type: 'tool_call' as const },
|
|
287
|
+
],
|
|
288
|
+
}),
|
|
289
|
+
new ToolMessage({ content: 'r2', tool_call_id: 'c2' }),
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const result = ensureThinkingBlockInMessages(messages, Providers.BEDROCK);
|
|
293
|
+
|
|
294
|
+
// Index 3 should NOT be converted — index 1 has reasoning in additional_kwargs
|
|
295
|
+
expect(result).toHaveLength(5);
|
|
296
|
+
expect(result[3]).toBeInstanceOf(AIMessage);
|
|
297
|
+
});
|
|
298
|
+
|
|
92
299
|
test('should not modify AI message with reasoning block and tool calls', () => {
|
|
93
300
|
const messages = [
|
|
94
301
|
new HumanMessage({ content: 'Calculate something' }),
|
package/src/messages/format.ts
CHANGED
|
@@ -1011,7 +1011,12 @@ export function ensureThinkingBlockInMessages(
|
|
|
1011
1011
|
|
|
1012
1012
|
while (i < messages.length) {
|
|
1013
1013
|
const msg = messages[i];
|
|
1014
|
-
|
|
1014
|
+
/** Detect AI messages by instanceof OR by role, in case cache-control cloning
|
|
1015
|
+
produced a plain object that lost the LangChain prototype. */
|
|
1016
|
+
const isAI =
|
|
1017
|
+
msg instanceof AIMessage ||
|
|
1018
|
+
msg instanceof AIMessageChunk ||
|
|
1019
|
+
('role' in msg && (msg as any).role === 'assistant');
|
|
1015
1020
|
|
|
1016
1021
|
if (!isAI) {
|
|
1017
1022
|
result.push(msg);
|
|
@@ -1025,30 +1030,58 @@ export function ensureThinkingBlockInMessages(
|
|
|
1025
1030
|
|
|
1026
1031
|
// Check if the message has tool calls or tool_use content
|
|
1027
1032
|
let hasToolUse = hasToolCalls ?? false;
|
|
1028
|
-
let
|
|
1033
|
+
let hasThinkingBlock = false;
|
|
1029
1034
|
|
|
1030
1035
|
if (contentIsArray && aiMsg.content.length > 0) {
|
|
1031
1036
|
const content = aiMsg.content as ExtendedMessageContent[];
|
|
1032
|
-
firstContentType = content[0]?.type;
|
|
1033
1037
|
hasToolUse =
|
|
1034
1038
|
hasToolUse ||
|
|
1035
1039
|
content.some((c) => typeof c === 'object' && c.type === 'tool_use');
|
|
1040
|
+
// Check ALL content blocks for thinking/reasoning, not just [0].
|
|
1041
|
+
// Bedrock may emit a whitespace text chunk before the thinking block,
|
|
1042
|
+
// pushing the reasoning_content to index 1+.
|
|
1043
|
+
hasThinkingBlock = content.some(
|
|
1044
|
+
(c) =>
|
|
1045
|
+
typeof c === 'object' &&
|
|
1046
|
+
(c.type === ContentTypes.THINKING ||
|
|
1047
|
+
c.type === ContentTypes.REASONING_CONTENT ||
|
|
1048
|
+
c.type === ContentTypes.REASONING ||
|
|
1049
|
+
c.type === 'redacted_thinking')
|
|
1050
|
+
);
|
|
1036
1051
|
}
|
|
1037
1052
|
|
|
1038
|
-
//
|
|
1053
|
+
// Bedrock also stores reasoning in additional_kwargs (may not be in content array)
|
|
1039
1054
|
if (
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
firstContentType !== ContentTypes.REASONING_CONTENT &&
|
|
1043
|
-
firstContentType !== ContentTypes.REASONING &&
|
|
1044
|
-
firstContentType !== 'redacted_thinking'
|
|
1055
|
+
!hasThinkingBlock &&
|
|
1056
|
+
aiMsg.additional_kwargs.reasoning_content != null
|
|
1045
1057
|
) {
|
|
1058
|
+
hasThinkingBlock = true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// If message has tool use but no thinking block, check whether this is a
|
|
1062
|
+
// continuation of a thinking-enabled agent's chain before converting.
|
|
1063
|
+
// Bedrock reasoning models can produce multiple AI→Tool rounds after an
|
|
1064
|
+
// initial reasoning response: the first AI message has reasoning_content,
|
|
1065
|
+
// but follow-ups have content: "" with only tool_calls. These are the
|
|
1066
|
+
// same agent's turn and must NOT be converted to HumanMessages.
|
|
1067
|
+
if (hasToolUse && !hasThinkingBlock) {
|
|
1068
|
+
// Walk backwards — if an earlier AI message in the same chain (before
|
|
1069
|
+
// the nearest HumanMessage) has a thinking/reasoning block, this is a
|
|
1070
|
+
// continuation of a thinking-enabled turn, not a non-thinking handoff.
|
|
1071
|
+
if (chainHasThinkingBlock(messages, i)) {
|
|
1072
|
+
result.push(msg);
|
|
1073
|
+
i++;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1046
1077
|
// Collect the AI message and any following tool messages
|
|
1047
1078
|
const toolSequence: BaseMessage[] = [msg];
|
|
1048
1079
|
let j = i + 1;
|
|
1049
1080
|
|
|
1050
1081
|
// Look ahead for tool messages that belong to this AI message
|
|
1051
|
-
|
|
1082
|
+
const isToolMsg = (m: BaseMessage): boolean =>
|
|
1083
|
+
m instanceof ToolMessage || ('role' in m && (m as any).role === 'tool');
|
|
1084
|
+
while (j < messages.length && isToolMsg(messages[j])) {
|
|
1052
1085
|
toolSequence.push(messages[j]);
|
|
1053
1086
|
j++;
|
|
1054
1087
|
}
|
|
@@ -1073,3 +1106,66 @@ export function ensureThinkingBlockInMessages(
|
|
|
1073
1106
|
|
|
1074
1107
|
return result;
|
|
1075
1108
|
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Walks backwards from `currentIndex` through the message array to check
|
|
1112
|
+
* whether an earlier AI message in the same "chain" (no HumanMessage boundary)
|
|
1113
|
+
* contains a thinking/reasoning block.
|
|
1114
|
+
*
|
|
1115
|
+
* A "chain" is a contiguous sequence of AI + Tool messages with no intervening
|
|
1116
|
+
* HumanMessage. Bedrock reasoning models produce reasoning on the first AI
|
|
1117
|
+
* response, then issue follow-up tool calls with `content: ""` and no
|
|
1118
|
+
* reasoning block. These follow-ups are part of the same thinking-enabled
|
|
1119
|
+
* turn and should not be converted.
|
|
1120
|
+
*/
|
|
1121
|
+
function chainHasThinkingBlock(
|
|
1122
|
+
messages: BaseMessage[],
|
|
1123
|
+
currentIndex: number
|
|
1124
|
+
): boolean {
|
|
1125
|
+
for (let k = currentIndex - 1; k >= 0; k--) {
|
|
1126
|
+
const prev = messages[k];
|
|
1127
|
+
|
|
1128
|
+
// HumanMessage = turn boundary — stop searching
|
|
1129
|
+
if (
|
|
1130
|
+
prev instanceof HumanMessage ||
|
|
1131
|
+
('role' in prev && (prev as any).role === 'user')
|
|
1132
|
+
) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Check AI messages for thinking/reasoning blocks
|
|
1137
|
+
const isPrevAI =
|
|
1138
|
+
prev instanceof AIMessage ||
|
|
1139
|
+
prev instanceof AIMessageChunk ||
|
|
1140
|
+
('role' in prev && (prev as any).role === 'assistant');
|
|
1141
|
+
|
|
1142
|
+
if (isPrevAI) {
|
|
1143
|
+
const prevAiMsg = prev as AIMessage | AIMessageChunk;
|
|
1144
|
+
|
|
1145
|
+
if (Array.isArray(prevAiMsg.content) && prevAiMsg.content.length > 0) {
|
|
1146
|
+
const content = prevAiMsg.content as ExtendedMessageContent[];
|
|
1147
|
+
if (
|
|
1148
|
+
content.some(
|
|
1149
|
+
(c) =>
|
|
1150
|
+
typeof c === 'object' &&
|
|
1151
|
+
(c.type === ContentTypes.THINKING ||
|
|
1152
|
+
c.type === ContentTypes.REASONING_CONTENT ||
|
|
1153
|
+
c.type === ContentTypes.REASONING ||
|
|
1154
|
+
c.type === 'redacted_thinking')
|
|
1155
|
+
)
|
|
1156
|
+
) {
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Bedrock also stores reasoning in additional_kwargs
|
|
1162
|
+
if (prevAiMsg.additional_kwargs.reasoning_content != null) {
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ToolMessages are part of the chain — keep walking back
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
package/src/tools/ToolSearch.ts
CHANGED
|
@@ -687,9 +687,23 @@ function getBaseToolName(toolName: string): string {
|
|
|
687
687
|
return toolName.substring(0, delimiterIndex);
|
|
688
688
|
}
|
|
689
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Checks whether a tool has any defined parameters in its JSON schema.
|
|
692
|
+
* @param parameters - The tool's JSON schema parameters
|
|
693
|
+
* @returns true if the tool has at least one parameter property
|
|
694
|
+
*/
|
|
695
|
+
function hasParams(parameters?: t.JsonSchemaType): boolean {
|
|
696
|
+
return (
|
|
697
|
+
parameters?.properties != null &&
|
|
698
|
+
Object.keys(parameters.properties).length > 0
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
690
702
|
/**
|
|
691
703
|
* Generates a compact listing of deferred tools grouped by server.
|
|
692
|
-
* Format: "server: tool1, tool2, tool3"
|
|
704
|
+
* Format: "server: tool1, tool2(\u2026), tool3"
|
|
705
|
+
* Tools with parameters are annotated with (\u2026) to signal
|
|
706
|
+
* that the LLM should discover the schema via tool_search before calling.
|
|
693
707
|
* Non-MCP tools are grouped under "other".
|
|
694
708
|
* @param toolRegistry - The tool registry
|
|
695
709
|
* @param onlyDeferred - Whether to only include deferred tools
|
|
@@ -713,11 +727,14 @@ function getDeferredToolsListing(
|
|
|
713
727
|
const toolName = lcTool.name;
|
|
714
728
|
const serverName = extractMcpServerName(toolName) ?? 'other';
|
|
715
729
|
const baseName = getBaseToolName(toolName);
|
|
730
|
+
const displayName = hasParams(lcTool.parameters)
|
|
731
|
+
? `${baseName}(\u2026)`
|
|
732
|
+
: baseName;
|
|
716
733
|
|
|
717
734
|
if (!(serverName in toolsByServer)) {
|
|
718
735
|
toolsByServer[serverName] = [];
|
|
719
736
|
}
|
|
720
|
-
toolsByServer[serverName].push(
|
|
737
|
+
toolsByServer[serverName].push(displayName);
|
|
721
738
|
}
|
|
722
739
|
|
|
723
740
|
const serverNames = Object.keys(toolsByServer).sort((a, b) => {
|
|
@@ -862,7 +879,7 @@ function createToolSearch(
|
|
|
862
879
|
deferredToolsListing.length > 0
|
|
863
880
|
? `
|
|
864
881
|
|
|
865
|
-
Deferred tools (search to load):
|
|
882
|
+
Deferred tools (search to load; \u2026 = has params, search first):
|
|
866
883
|
${deferredToolsListing}`
|
|
867
884
|
: '';
|
|
868
885
|
|
|
@@ -664,6 +664,11 @@ describe('ToolSearch', () => {
|
|
|
664
664
|
name: 'get_weather_mcp_weather-api',
|
|
665
665
|
description: 'Get weather',
|
|
666
666
|
defer_loading: true,
|
|
667
|
+
parameters: {
|
|
668
|
+
type: 'object',
|
|
669
|
+
properties: { location: { type: 'string' } },
|
|
670
|
+
required: ['location'],
|
|
671
|
+
},
|
|
667
672
|
});
|
|
668
673
|
registry.set('get_forecast_mcp_weather-api', {
|
|
669
674
|
name: 'get_forecast_mcp_weather-api',
|
|
@@ -674,6 +679,11 @@ describe('ToolSearch', () => {
|
|
|
674
679
|
name: 'send_email_mcp_gmail',
|
|
675
680
|
description: 'Send email',
|
|
676
681
|
defer_loading: true,
|
|
682
|
+
parameters: {
|
|
683
|
+
type: 'object',
|
|
684
|
+
properties: { to: { type: 'string' }, body: { type: 'string' } },
|
|
685
|
+
required: ['to', 'body'],
|
|
686
|
+
},
|
|
677
687
|
});
|
|
678
688
|
registry.set('execute_code', {
|
|
679
689
|
name: 'execute_code',
|
|
@@ -692,8 +702,10 @@ describe('ToolSearch', () => {
|
|
|
692
702
|
const registry = createRegistry();
|
|
693
703
|
const listing = getDeferredToolsListing(registry, true);
|
|
694
704
|
|
|
695
|
-
expect(listing).toContain('gmail: send_email');
|
|
696
|
-
expect(listing).toContain(
|
|
705
|
+
expect(listing).toContain('gmail: send_email(\u2026)');
|
|
706
|
+
expect(listing).toContain(
|
|
707
|
+
'weather-api: get_weather(\u2026), get_forecast'
|
|
708
|
+
);
|
|
697
709
|
expect(listing).toContain('other: execute_code');
|
|
698
710
|
});
|
|
699
711
|
|
|
@@ -715,6 +727,54 @@ describe('ToolSearch', () => {
|
|
|
715
727
|
expect(listing).not.toContain('get_weather_mcp_weather-api');
|
|
716
728
|
});
|
|
717
729
|
|
|
730
|
+
it('annotates tools with any params with ellipsis', () => {
|
|
731
|
+
const registry = createRegistry();
|
|
732
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
733
|
+
|
|
734
|
+
// Tools with params get (…)
|
|
735
|
+
expect(listing).toContain('get_weather(\u2026)');
|
|
736
|
+
expect(listing).toContain('send_email(\u2026)');
|
|
737
|
+
|
|
738
|
+
// Tools without params stay clean
|
|
739
|
+
expect(listing).toContain('get_forecast');
|
|
740
|
+
expect(listing).not.toContain('get_forecast(\u2026)');
|
|
741
|
+
expect(listing).toContain('execute_code');
|
|
742
|
+
expect(listing).not.toContain('execute_code(\u2026)');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('annotates tools with optional-only params', () => {
|
|
746
|
+
const registry: LCToolRegistry = new Map();
|
|
747
|
+
registry.set('list_items_mcp_api', {
|
|
748
|
+
name: 'list_items_mcp_api',
|
|
749
|
+
description: 'List items',
|
|
750
|
+
defer_loading: true,
|
|
751
|
+
parameters: {
|
|
752
|
+
type: 'object',
|
|
753
|
+
properties: { limit: { type: 'integer' } },
|
|
754
|
+
required: [],
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
759
|
+
expect(listing).toBe('api: list_items(\u2026)');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('does not annotate tools with empty properties', () => {
|
|
763
|
+
const registry: LCToolRegistry = new Map();
|
|
764
|
+
registry.set('ping_mcp_api', {
|
|
765
|
+
name: 'ping_mcp_api',
|
|
766
|
+
description: 'Ping',
|
|
767
|
+
defer_loading: true,
|
|
768
|
+
parameters: {
|
|
769
|
+
type: 'object',
|
|
770
|
+
properties: {},
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
const listing = getDeferredToolsListing(registry, true);
|
|
775
|
+
expect(listing).toBe('api: ping');
|
|
776
|
+
});
|
|
777
|
+
|
|
718
778
|
it('respects onlyDeferred flag', () => {
|
|
719
779
|
const registry = createRegistry();
|
|
720
780
|
|