@librechat/agents 3.1.26 → 3.1.28
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 +8 -2
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/main.cjs +8 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +134 -67
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +29 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +40 -0
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +8 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +129 -62
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +26 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +38 -2
- package/dist/esm/tools/ToolSearch.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +2 -1
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +43 -0
- package/dist/types/tools/ToolSearch.d.ts +85 -0
- package/dist/types/types/graph.d.ts +6 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +10 -0
- package/src/messages/format.ts +163 -70
- package/src/messages/formatAgentMessages.test.ts +369 -0
- package/src/messages/formatAgentMessages.tools.test.ts +68 -49
- package/src/tools/ProgrammaticToolCalling.ts +29 -1
- package/src/tools/ToolSearch.ts +43 -0
- package/src/types/graph.ts +6 -0
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
2
|
import type * as t from '@/types';
|
|
3
|
+
import { Constants } from '@/common';
|
|
4
|
+
export declare const ToolSearchToolName = Constants.TOOL_SEARCH;
|
|
5
|
+
export declare const ToolSearchToolDescription = "Searches deferred tools using BM25 ranking. Multi-word queries supported. Use mcp_server param to filter by server.";
|
|
6
|
+
export declare const ToolSearchToolSchema: {
|
|
7
|
+
readonly type: "object";
|
|
8
|
+
readonly properties: {
|
|
9
|
+
readonly query: {
|
|
10
|
+
readonly type: "string";
|
|
11
|
+
readonly maxLength: 200;
|
|
12
|
+
readonly default: "";
|
|
13
|
+
readonly description: "Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.";
|
|
14
|
+
};
|
|
15
|
+
readonly fields: {
|
|
16
|
+
readonly type: "array";
|
|
17
|
+
readonly items: {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
readonly enum: readonly ["name", "description", "parameters"];
|
|
20
|
+
};
|
|
21
|
+
readonly default: readonly ["name", "description"];
|
|
22
|
+
readonly description: "Which fields to search. Default: name and description";
|
|
23
|
+
};
|
|
24
|
+
readonly max_results: {
|
|
25
|
+
readonly type: "integer";
|
|
26
|
+
readonly minimum: 1;
|
|
27
|
+
readonly maximum: 50;
|
|
28
|
+
readonly default: 10;
|
|
29
|
+
readonly description: "Maximum number of matching tools to return";
|
|
30
|
+
};
|
|
31
|
+
readonly mcp_server: {
|
|
32
|
+
readonly oneOf: readonly [{
|
|
33
|
+
readonly type: "string";
|
|
34
|
+
}, {
|
|
35
|
+
readonly type: "array";
|
|
36
|
+
readonly items: {
|
|
37
|
+
readonly type: "string";
|
|
38
|
+
};
|
|
39
|
+
}];
|
|
40
|
+
readonly description: "Filter to tools from specific MCP server(s). Can be a single server name or array of names. If provided without a query, lists all tools from those servers.";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
readonly required: readonly [];
|
|
44
|
+
};
|
|
45
|
+
export declare const ToolSearchToolDefinition: {
|
|
46
|
+
readonly name: Constants.TOOL_SEARCH;
|
|
47
|
+
readonly description: "Searches deferred tools using BM25 ranking. Multi-word queries supported. Use mcp_server param to filter by server.";
|
|
48
|
+
readonly schema: {
|
|
49
|
+
readonly type: "object";
|
|
50
|
+
readonly properties: {
|
|
51
|
+
readonly query: {
|
|
52
|
+
readonly type: "string";
|
|
53
|
+
readonly maxLength: 200;
|
|
54
|
+
readonly default: "";
|
|
55
|
+
readonly description: "Search term to find in tool names and descriptions. Case-insensitive substring matching. Optional if mcp_server is provided.";
|
|
56
|
+
};
|
|
57
|
+
readonly fields: {
|
|
58
|
+
readonly type: "array";
|
|
59
|
+
readonly items: {
|
|
60
|
+
readonly type: "string";
|
|
61
|
+
readonly enum: readonly ["name", "description", "parameters"];
|
|
62
|
+
};
|
|
63
|
+
readonly default: readonly ["name", "description"];
|
|
64
|
+
readonly description: "Which fields to search. Default: name and description";
|
|
65
|
+
};
|
|
66
|
+
readonly max_results: {
|
|
67
|
+
readonly type: "integer";
|
|
68
|
+
readonly minimum: 1;
|
|
69
|
+
readonly maximum: 50;
|
|
70
|
+
readonly default: 10;
|
|
71
|
+
readonly description: "Maximum number of matching tools to return";
|
|
72
|
+
};
|
|
73
|
+
readonly mcp_server: {
|
|
74
|
+
readonly oneOf: readonly [{
|
|
75
|
+
readonly type: "string";
|
|
76
|
+
}, {
|
|
77
|
+
readonly type: "array";
|
|
78
|
+
readonly items: {
|
|
79
|
+
readonly type: "string";
|
|
80
|
+
};
|
|
81
|
+
}];
|
|
82
|
+
readonly description: "Filter to tools from specific MCP server(s). Can be a single server name or array of names. If provided without a query, lists all tools from those servers.";
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
readonly required: readonly [];
|
|
86
|
+
};
|
|
87
|
+
};
|
|
3
88
|
/**
|
|
4
89
|
* Extracts the MCP server name from a tool name.
|
|
5
90
|
* MCP tools follow the pattern: toolName_mcp_serverName
|
|
@@ -262,4 +262,10 @@ export interface AgentInputs {
|
|
|
262
262
|
* ON_TOOL_EXECUTE events instead of invoking tools directly.
|
|
263
263
|
*/
|
|
264
264
|
toolDefinitions?: LCTool[];
|
|
265
|
+
/**
|
|
266
|
+
* Tool names discovered from previous conversation history.
|
|
267
|
+
* These tools will be pre-marked as discovered so they're included
|
|
268
|
+
* in tool binding without requiring tool_search.
|
|
269
|
+
*/
|
|
270
|
+
discoveredTools?: string[];
|
|
265
271
|
}
|
package/package.json
CHANGED
|
@@ -42,6 +42,7 @@ export class AgentContext {
|
|
|
42
42
|
maxContextTokens,
|
|
43
43
|
reasoningKey,
|
|
44
44
|
useLegacyContent,
|
|
45
|
+
discoveredTools,
|
|
45
46
|
} = agentConfig;
|
|
46
47
|
|
|
47
48
|
const agentContext = new AgentContext({
|
|
@@ -62,6 +63,7 @@ export class AgentContext {
|
|
|
62
63
|
instructionTokens: 0,
|
|
63
64
|
tokenCounter,
|
|
64
65
|
useLegacyContent,
|
|
66
|
+
discoveredTools,
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
if (tokenCounter) {
|
|
@@ -186,6 +188,7 @@ export class AgentContext {
|
|
|
186
188
|
toolEnd,
|
|
187
189
|
instructionTokens,
|
|
188
190
|
useLegacyContent,
|
|
191
|
+
discoveredTools,
|
|
189
192
|
}: {
|
|
190
193
|
agentId: string;
|
|
191
194
|
name?: string;
|
|
@@ -204,6 +207,7 @@ export class AgentContext {
|
|
|
204
207
|
toolEnd?: boolean;
|
|
205
208
|
instructionTokens?: number;
|
|
206
209
|
useLegacyContent?: boolean;
|
|
210
|
+
discoveredTools?: string[];
|
|
207
211
|
}) {
|
|
208
212
|
this.agentId = agentId;
|
|
209
213
|
this.name = name;
|
|
@@ -229,6 +233,12 @@ export class AgentContext {
|
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
this.useLegacyContent = useLegacyContent ?? false;
|
|
236
|
+
|
|
237
|
+
if (discoveredTools && discoveredTools.length > 0) {
|
|
238
|
+
for (const toolName of discoveredTools) {
|
|
239
|
+
this.discoveredToolNames.add(toolName);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
232
242
|
}
|
|
233
243
|
|
|
234
244
|
/**
|
package/src/messages/format.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
TPayload,
|
|
20
20
|
TMessage,
|
|
21
21
|
} from '@/types';
|
|
22
|
-
import { Providers, ContentTypes } from '@/common';
|
|
22
|
+
import { Providers, ContentTypes, Constants } from '@/common';
|
|
23
23
|
|
|
24
24
|
interface MediaMessageParams {
|
|
25
25
|
message: {
|
|
@@ -297,7 +297,7 @@ function formatAssistantMessage(
|
|
|
297
297
|
if (currentContent.length > 0) {
|
|
298
298
|
let content = currentContent.reduce((acc, curr) => {
|
|
299
299
|
if (curr.type === ContentTypes.TEXT) {
|
|
300
|
-
return `${acc}${curr[ContentTypes.TEXT]
|
|
300
|
+
return `${acc}${String(curr[ContentTypes.TEXT] ?? '')}\n`;
|
|
301
301
|
}
|
|
302
302
|
return acc;
|
|
303
303
|
}, '');
|
|
@@ -384,7 +384,7 @@ function formatAssistantMessage(
|
|
|
384
384
|
const content = currentContent
|
|
385
385
|
.reduce((acc, curr) => {
|
|
386
386
|
if (curr.type === ContentTypes.TEXT) {
|
|
387
|
-
return `${acc}${curr[ContentTypes.TEXT]
|
|
387
|
+
return `${acc}${String(curr[ContentTypes.TEXT] ?? '')}\n`;
|
|
388
388
|
}
|
|
389
389
|
return acc;
|
|
390
390
|
}, '')
|
|
@@ -620,6 +620,48 @@ export const labelContentByAgent = (
|
|
|
620
620
|
return result;
|
|
621
621
|
};
|
|
622
622
|
|
|
623
|
+
/** Extracts tool names from a tool_search output JSON string. */
|
|
624
|
+
function extractToolNamesFromSearchOutput(output: string): string[] {
|
|
625
|
+
try {
|
|
626
|
+
const parsed: unknown = JSON.parse(output);
|
|
627
|
+
if (
|
|
628
|
+
typeof parsed === 'object' &&
|
|
629
|
+
parsed !== null &&
|
|
630
|
+
Array.isArray((parsed as Record<string, unknown>).tools)
|
|
631
|
+
) {
|
|
632
|
+
return (
|
|
633
|
+
(parsed as Record<string, unknown>).tools as Array<{ name?: string }>
|
|
634
|
+
)
|
|
635
|
+
.map((t) => t.name)
|
|
636
|
+
.filter((name): name is string => typeof name === 'string');
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
639
|
+
/** Output may have warnings prepended, try to find JSON within it */
|
|
640
|
+
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
641
|
+
if (jsonMatch) {
|
|
642
|
+
try {
|
|
643
|
+
const parsed: unknown = JSON.parse(jsonMatch[0]);
|
|
644
|
+
if (
|
|
645
|
+
typeof parsed === 'object' &&
|
|
646
|
+
parsed !== null &&
|
|
647
|
+
Array.isArray((parsed as Record<string, unknown>).tools)
|
|
648
|
+
) {
|
|
649
|
+
return (
|
|
650
|
+
(parsed as Record<string, unknown>).tools as Array<{
|
|
651
|
+
name?: string;
|
|
652
|
+
}>
|
|
653
|
+
)
|
|
654
|
+
.map((t) => t.name)
|
|
655
|
+
.filter((name): name is string => typeof name === 'string');
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
/* ignore */
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
|
|
623
665
|
/**
|
|
624
666
|
* Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
|
|
625
667
|
*
|
|
@@ -644,6 +686,13 @@ export const formatAgentMessages = (
|
|
|
644
686
|
// Keep track of the mapping from original payload indices to result indices
|
|
645
687
|
const indexMapping: Record<number, number[] | undefined> = {};
|
|
646
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Create a mutable copy of the tools set that can be expanded dynamically.
|
|
691
|
+
* When we encounter tool_search results, we add discovered tools to this set,
|
|
692
|
+
* making their subsequent tool calls valid.
|
|
693
|
+
*/
|
|
694
|
+
const discoveredTools = tools ? new Set(tools) : undefined;
|
|
695
|
+
|
|
647
696
|
// Process messages with tool conversion if tools set is provided
|
|
648
697
|
for (let i = 0; i < payload.length; i++) {
|
|
649
698
|
const message = payload[i];
|
|
@@ -670,96 +719,140 @@ export const formatAgentMessages = (
|
|
|
670
719
|
// For assistant messages, track the starting index before processing
|
|
671
720
|
const startMessageIndex = messages.length;
|
|
672
721
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
722
|
+
/**
|
|
723
|
+
* If tools set is provided, process tool_calls:
|
|
724
|
+
* - Keep valid tool_calls (tools in the set or dynamically discovered)
|
|
725
|
+
* - Convert invalid tool_calls to string representation for context preservation
|
|
726
|
+
* - Dynamically expand the set when tool_search results are encountered
|
|
727
|
+
*/
|
|
728
|
+
let processedMessage = message;
|
|
729
|
+
if (discoveredTools) {
|
|
680
730
|
const content = message.content;
|
|
681
731
|
if (content && Array.isArray(content)) {
|
|
732
|
+
const filteredContent: typeof content = [];
|
|
733
|
+
const invalidToolCallIds = new Set<string>();
|
|
734
|
+
const invalidToolStrings: string[] = [];
|
|
735
|
+
|
|
682
736
|
for (const part of content) {
|
|
683
|
-
if (part.type
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
737
|
+
if (part.type !== ContentTypes.TOOL_CALL) {
|
|
738
|
+
filteredContent.push(part);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Skip malformed tool_call entries */
|
|
743
|
+
if (
|
|
744
|
+
part.tool_call == null ||
|
|
745
|
+
part.tool_call.name == null ||
|
|
746
|
+
part.tool_call.name === ''
|
|
747
|
+
) {
|
|
690
748
|
if (
|
|
691
|
-
part.tool_call
|
|
692
|
-
part.tool_call.
|
|
693
|
-
part.tool_call.name === ''
|
|
749
|
+
typeof part.tool_call?.id === 'string' &&
|
|
750
|
+
part.tool_call.id !== ''
|
|
694
751
|
) {
|
|
695
|
-
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
const toolName = part.tool_call.name;
|
|
699
|
-
toolNames.push(toolName);
|
|
700
|
-
if (!tools.has(toolName)) {
|
|
701
|
-
hasInvalidTool = true;
|
|
752
|
+
invalidToolCallIds.add(part.tool_call.id);
|
|
702
753
|
}
|
|
754
|
+
continue;
|
|
703
755
|
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
756
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (content != null && Array.isArray(content)) {
|
|
724
|
-
for (const part of content) {
|
|
725
|
-
if (part.type === ContentTypes.TOOL_CALL) {
|
|
726
|
-
isToolResponse = true;
|
|
727
|
-
break;
|
|
728
|
-
}
|
|
757
|
+
const toolName = part.tool_call.name;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* If this is a tool_search result with output, extract discovered tool names
|
|
761
|
+
* and add them to the discoveredTools set for subsequent validation.
|
|
762
|
+
*/
|
|
763
|
+
if (
|
|
764
|
+
toolName === Constants.TOOL_SEARCH &&
|
|
765
|
+
typeof part.tool_call.output === 'string' &&
|
|
766
|
+
part.tool_call.output !== ''
|
|
767
|
+
) {
|
|
768
|
+
const extracted = extractToolNamesFromSearchOutput(
|
|
769
|
+
part.tool_call.output
|
|
770
|
+
);
|
|
771
|
+
for (const name of extracted) {
|
|
772
|
+
discoveredTools.add(name);
|
|
729
773
|
}
|
|
730
774
|
}
|
|
731
775
|
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
toolSequence.push(...nextMessages);
|
|
736
|
-
sequenceEndIndex = j;
|
|
737
|
-
j++;
|
|
776
|
+
if (discoveredTools.has(toolName)) {
|
|
777
|
+
/** Valid tool - keep it */
|
|
778
|
+
filteredContent.push(part);
|
|
738
779
|
} else {
|
|
739
|
-
|
|
740
|
-
|
|
780
|
+
/** Invalid tool - convert to string for context preservation */
|
|
781
|
+
if (
|
|
782
|
+
typeof part.tool_call.id === 'string' &&
|
|
783
|
+
part.tool_call.id !== ''
|
|
784
|
+
) {
|
|
785
|
+
invalidToolCallIds.add(part.tool_call.id);
|
|
786
|
+
}
|
|
787
|
+
const output = part.tool_call.output ?? '';
|
|
788
|
+
invalidToolStrings.push(`Tool: ${toolName}, ${output}`);
|
|
741
789
|
}
|
|
742
790
|
}
|
|
743
791
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
792
|
+
/** Remove tool_call_ids references to invalid tools from text parts */
|
|
793
|
+
if (invalidToolCallIds.size > 0) {
|
|
794
|
+
for (const part of filteredContent) {
|
|
795
|
+
if (
|
|
796
|
+
part.type === ContentTypes.TEXT &&
|
|
797
|
+
Array.isArray(part.tool_call_ids)
|
|
798
|
+
) {
|
|
799
|
+
part.tool_call_ids = part.tool_call_ids.filter(
|
|
800
|
+
(id: string) => !invalidToolCallIds.has(id)
|
|
801
|
+
);
|
|
802
|
+
if (part.tool_call_ids.length === 0) {
|
|
803
|
+
delete part.tool_call_ids;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
747
808
|
|
|
748
|
-
|
|
749
|
-
|
|
809
|
+
/** Append invalid tool strings to the content for context preservation */
|
|
810
|
+
if (invalidToolStrings.length > 0) {
|
|
811
|
+
/** Find the last text part or create one */
|
|
812
|
+
let lastTextPartIndex = -1;
|
|
813
|
+
for (let j = filteredContent.length - 1; j >= 0; j--) {
|
|
814
|
+
if (filteredContent[j].type === ContentTypes.TEXT) {
|
|
815
|
+
lastTextPartIndex = j;
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
750
819
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
820
|
+
const invalidToolText = invalidToolStrings.join('\n');
|
|
821
|
+
if (lastTextPartIndex >= 0) {
|
|
822
|
+
const lastTextPart = filteredContent[lastTextPartIndex] as {
|
|
823
|
+
type: string;
|
|
824
|
+
[ContentTypes.TEXT]?: string;
|
|
825
|
+
text?: string;
|
|
826
|
+
};
|
|
827
|
+
const existingText =
|
|
828
|
+
lastTextPart[ContentTypes.TEXT] ?? lastTextPart.text ?? '';
|
|
829
|
+
filteredContent[lastTextPartIndex] = {
|
|
830
|
+
...lastTextPart,
|
|
831
|
+
[ContentTypes.TEXT]: existingText
|
|
832
|
+
? `${existingText}\n${invalidToolText}`
|
|
833
|
+
: invalidToolText,
|
|
834
|
+
};
|
|
835
|
+
} else {
|
|
836
|
+
/** No text part exists, create one */
|
|
837
|
+
filteredContent.push({
|
|
838
|
+
type: ContentTypes.TEXT,
|
|
839
|
+
[ContentTypes.TEXT]: invalidToolText,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
755
842
|
}
|
|
756
843
|
|
|
757
|
-
|
|
844
|
+
/** Use filtered content if we made any changes */
|
|
845
|
+
if (
|
|
846
|
+
filteredContent.length !== content.length ||
|
|
847
|
+
invalidToolStrings.length > 0
|
|
848
|
+
) {
|
|
849
|
+
processedMessage = { ...message, content: filteredContent };
|
|
850
|
+
}
|
|
758
851
|
}
|
|
759
852
|
}
|
|
760
853
|
|
|
761
854
|
// Process the assistant message using the helper function
|
|
762
|
-
const formattedMessages = formatAssistantMessage(
|
|
855
|
+
const formattedMessages = formatAssistantMessage(processedMessage);
|
|
763
856
|
messages.push(...formattedMessages);
|
|
764
857
|
|
|
765
858
|
// Update the index mapping for this assistant message
|