@librechat/agents 3.1.41 → 3.1.43

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.
@@ -821,6 +821,112 @@ describe('Agent Handoffs Tests', () => {
821
821
  });
822
822
  });
823
823
 
824
+ describe('Tool Call Before Handoff (Issue #54)', () => {
825
+ it('should complete handoff when router calls a non-handoff tool in the same turn', async () => {
826
+ /**
827
+ * Reproduces the bug from issue #54:
828
+ * When a router calls a regular tool AND a handoff tool in the same turn,
829
+ * the filtered messages for the receiving agent end with a ToolMessage.
830
+ * Previously, instructions were appended as a HumanMessage (tool → user),
831
+ * which many APIs reject. The fix injects instructions into the last
832
+ * ToolMessage instead.
833
+ */
834
+ const customTool = new DynamicStructuredTool({
835
+ name: 'list_upload_sessions',
836
+ description: 'List available upload sessions',
837
+ schema: { type: 'object', properties: {}, required: [] },
838
+ func: async (): Promise<string> =>
839
+ JSON.stringify({ sessions: [{ id: 'sess_1', status: 'ready' }] }),
840
+ });
841
+
842
+ const agents: t.AgentInputs[] = [
843
+ {
844
+ ...createBasicAgent('router', 'You are a router'),
845
+ tools: [customTool],
846
+ toolMap: new Map([['list_upload_sessions', customTool]]) as t.ToolMap,
847
+ },
848
+ createBasicAgent('data_analyst', 'You are a data analyst'),
849
+ ];
850
+
851
+ const edges: t.GraphEdge[] = [
852
+ {
853
+ from: 'router',
854
+ to: 'data_analyst',
855
+ edgeType: 'handoff',
856
+ description: 'Transfer to data analyst',
857
+ prompt: 'Instructions for the analyst about what to analyze',
858
+ promptKey: 'instructions',
859
+ },
860
+ ];
861
+
862
+ const run = await Run.create(createTestConfig(agents, edges));
863
+
864
+ /**
865
+ * Simulate router calling list_upload_sessions AND handoff in the same turn.
866
+ * The first model response includes both tool calls.
867
+ * The second model response is the data_analyst's reply.
868
+ */
869
+ run.Graph?.overrideTestModel(
870
+ [
871
+ 'Checking available sessions and transferring to analyst',
872
+ 'Here is my analysis of the available sessions',
873
+ ],
874
+ 10,
875
+ [
876
+ {
877
+ id: 'tool_call_1',
878
+ name: 'list_upload_sessions',
879
+ args: {},
880
+ } as ToolCall,
881
+ {
882
+ id: 'tool_call_2',
883
+ name: `${Constants.LC_TRANSFER_TO_}data_analyst`,
884
+ args: { instructions: 'Analyze the upload session data' },
885
+ } as ToolCall,
886
+ ]
887
+ );
888
+
889
+ const messages = [
890
+ new HumanMessage('Check my upload sessions and analyze them'),
891
+ ];
892
+
893
+ const config: Partial<RunnableConfig> & {
894
+ version: 'v1' | 'v2';
895
+ streamMode: string;
896
+ } = {
897
+ configurable: {
898
+ thread_id: 'test-tool-before-handoff-thread',
899
+ },
900
+ streamMode: 'values',
901
+ version: 'v2' as const,
902
+ };
903
+
904
+ /**
905
+ * This should complete without error. Before the fix, the receiving
906
+ * agent would get an invalid tool → user message sequence.
907
+ */
908
+ await run.processStream({ messages }, config);
909
+
910
+ const finalMessages = run.getRunMessages();
911
+ expect(finalMessages).toBeDefined();
912
+ expect(finalMessages!.length).toBeGreaterThan(1);
913
+
914
+ /** Verify that the handoff occurred */
915
+ const toolMessages = finalMessages!.filter(
916
+ (msg) => msg.getType() === 'tool'
917
+ ) as ToolMessage[];
918
+
919
+ const handoffMessage = toolMessages.find(
920
+ (msg) => msg.name === `${Constants.LC_TRANSFER_TO_}data_analyst`
921
+ );
922
+ expect(handoffMessage).toBeDefined();
923
+
924
+ /** Verify the flow completed (agent B responded) */
925
+ const aiMessages = finalMessages!.filter((msg) => msg.getType() === 'ai');
926
+ expect(aiMessages.length).toBeGreaterThanOrEqual(1);
927
+ });
928
+ });
929
+
824
930
  describe('Handoff Tool Naming', () => {
825
931
  it('should use correct naming convention for handoff tools', async () => {
826
932
  const agents: t.AgentInputs[] = [