@@ -3,6 +3,8 @@ const { Constants } = require('librechat-data-provider');
33const { updateSchedulerExecution } = require ( '~/models/SchedulerExecution' ) ;
44const { loadAgent } = require ( '~/models/Agent' ) ;
55const { User } = require ( '~/db/models' ) ;
6+ const { getBufferString } = require ( '@langchain/core/messages' ) ;
7+ const { HumanMessage } = require ( '@langchain/core/messages' ) ;
68const {
79 createMinimalMockResponse,
810 updateRequestForEphemeralAgent,
@@ -17,6 +19,117 @@ const {
1719} = require ( './executor' ) ;
1820const SchedulerClientFactory = require ( '~/server/services/Scheduler/SchedulerClientFactory' ) ;
1921
22+ /**
23+ * Extract meaningful content from step result object for display
24+ * @param {Object } result - Step result object
25+ * @returns {string|null } Meaningful content or null if not found
26+ */
27+ function extractMeaningfulContent ( result ) {
28+ if ( ! result || typeof result !== 'object' ) {
29+ return null ;
30+ }
31+
32+ // Handle LibreChat agent response objects with content array
33+ if ( result . content && Array . isArray ( result . content ) ) {
34+ // Extract text content from agent response content array
35+ const textParts = result . content
36+ . filter ( part => part . type === 'text' && part . text && part . text . trim ( ) )
37+ . map ( part => part . text . trim ( ) ) ;
38+
39+ if ( textParts . length > 0 ) {
40+ return textParts . join ( '\n' ) . trim ( ) ;
41+ }
42+ }
43+
44+ // Check for direct text field
45+ if ( result . text && typeof result . text === 'string' && result . text . trim ( ) ) {
46+ return result . text . trim ( ) ;
47+ }
48+
49+ // Handle nested agent response objects (common in LibreChat)
50+ if ( result . agentResponse ) {
51+ if ( typeof result . agentResponse === 'string' ) {
52+ return result . agentResponse ;
53+ }
54+ if ( typeof result . agentResponse === 'object' ) {
55+ // Try to extract text from nested agent response content
56+ if ( result . agentResponse . content && Array . isArray ( result . agentResponse . content ) ) {
57+ const textParts = result . agentResponse . content
58+ . filter ( part => part . type === 'text' && part . text && part . text . trim ( ) )
59+ . map ( part => part . text . trim ( ) ) ;
60+
61+ if ( textParts . length > 0 ) {
62+ return textParts . join ( '\n' ) . trim ( ) ;
63+ }
64+ }
65+
66+ // Check for direct text in agent response
67+ if ( result . agentResponse . text && typeof result . agentResponse . text === 'string' && result . agentResponse . text . trim ( ) ) {
68+ return result . agentResponse . text . trim ( ) ;
69+ }
70+
71+ // Check for message content in agent response
72+ if ( result . agentResponse . content && typeof result . agentResponse . content === 'string' ) {
73+ return result . agentResponse . content ;
74+ }
75+ }
76+ }
77+
78+ // Check for tool results
79+ if ( result . toolResults && Array . isArray ( result . toolResults ) ) {
80+ const meaningfulResults = result . toolResults
81+ . map ( tool => {
82+ if ( tool . result && typeof tool . result === 'string' ) {
83+ return `Tool "${ tool . name || 'unknown' } ": ${ tool . result } ` ;
84+ }
85+ if ( tool . result && typeof tool . result === 'object' ) {
86+ // Try to extract meaningful data from tool result
87+ if ( tool . result . data || tool . result . message || tool . result . content ) {
88+ const content = tool . result . data || tool . result . message || tool . result . content ;
89+ return `Tool "${ tool . name || 'unknown' } ": ${ typeof content === 'string' ? content : JSON . stringify ( content ) } ` ;
90+ }
91+ }
92+ return null ;
93+ } )
94+ . filter ( Boolean ) ;
95+
96+ if ( meaningfulResults . length > 0 ) {
97+ return meaningfulResults . join ( '\n' ) ;
98+ }
99+ }
100+
101+ // Check for direct content fields
102+ if ( result . content && typeof result . content === 'string' ) {
103+ return result . content ;
104+ }
105+
106+ if ( result . message && typeof result . message === 'string' ) {
107+ return result . message ;
108+ }
109+
110+ if ( result . data ) {
111+ if ( typeof result . data === 'string' ) {
112+ return result . data ;
113+ }
114+ if ( typeof result . data === 'object' ) {
115+ // Try to extract summary information from data objects
116+ if ( Array . isArray ( result . data ) ) {
117+ return `Retrieved ${ result . data . length } items` ;
118+ }
119+ if ( result . data . summary || result . data . description ) {
120+ return result . data . summary || result . data . description ;
121+ }
122+ }
123+ }
124+
125+ // Check for successful execution indicators
126+ if ( result . success && result . type ) {
127+ return `Successfully executed ${ result . type } operation` ;
128+ }
129+
130+ return null ;
131+ }
132+
20133/**
21134 * WorkflowExecutor - Handles the execution of workflows
22135 *
@@ -138,6 +251,7 @@ class WorkflowExecutor {
138251 const executionId = execution . id ;
139252 const userId = execution . user ;
140253 let executionContext = null ; // Initialize here to ensure it's in scope for error handling
254+ let stepMessages = [ ] ; // Initialize here to ensure it's in scope for error handling
141255
142256 try {
143257 logger . info ( `[WorkflowExecutor] Starting workflow execution: ${ workflowId } ` ) ;
@@ -229,7 +343,7 @@ class WorkflowExecutor {
229343 }
230344
231345 // Execute steps starting from the first step
232- const result = await this . executeStepChain ( workflow , execution , firstStep . id , executionContext , executionMetadata ) ;
346+ const result = await this . executeStepChain ( workflow , execution , firstStep . id , executionContext , executionMetadata , stepMessages ) ;
233347
234348 // Clean up tracking
235349 this . runningExecutions . delete ( executionId ) ;
@@ -242,11 +356,19 @@ class WorkflowExecutor {
242356
243357 logger . error ( `[WorkflowExecutor] Workflow execution failed: ${ workflowId } ` , error ) ;
244358
359+ // Create error summary with any partial results
360+ let errorOutput = `Workflow failed: ${ error . message } ` ;
361+ if ( executionContext && stepMessages && stepMessages . length > 0 ) {
362+ const partialSummary = getBufferString ( stepMessages ) ;
363+ errorOutput = `${ errorOutput } \n\nPartial results before failure:\n${ partialSummary } ` ;
364+ }
365+
245366 // Update execution status
246367 await updateSchedulerExecution ( executionId , execution . user , {
247368 status : 'failed' ,
248369 endTime : new Date ( ) ,
249370 error : error . message ,
371+ output : errorOutput ,
250372 } ) ;
251373
252374 return {
@@ -264,9 +386,10 @@ class WorkflowExecutor {
264386 * @param {string } currentStepId - Current step ID to execute
265387 * @param {Object } context - Execution context
266388 * @param {Object } metadata - Execution metadata with step tracking
389+ * @param {Array } stepMessages - Array to accumulate step outputs as messages
267390 * @returns {Promise<Object> } Execution result
268391 */
269- async executeStepChain ( workflow , execution , currentStepId , context , metadata ) {
392+ async executeStepChain ( workflow , execution , currentStepId , context , metadata , stepMessages ) {
270393 let currentStep = currentStepId ;
271394 const executionResult = { success : true , error : null } ;
272395 const accumulatedStepResults = [ ] ;
@@ -303,18 +426,73 @@ class WorkflowExecutor {
303426 } ) ;
304427 }
305428
429+ // Create input for the current step using buffer string approach
430+ let stepInput = context ;
431+ if ( stepMessages . length > 0 ) {
432+ // Convert accumulated step messages into a buffer string for the next step
433+ const bufferString = getBufferString ( stepMessages ) ;
434+
435+ // Add the buffer string as context for the current step
436+ stepInput = {
437+ ...context ,
438+ previousStepsOutput : bufferString ,
439+ steps : context . steps // Keep the structured step results for metadata
440+ } ;
441+
442+ logger . debug (
443+ `[WorkflowExecutor] Passing accumulated output to step ${ step . name } : ${ bufferString . substring ( 0 , 200 ) } ...` ,
444+ ) ;
445+ }
446+
306447 // Execute the current step (each step gets a fresh agent)
307- const stepResult = await executeStep ( workflow , execution , step , context , executionData . abortController ?. signal ) ;
448+ const stepResult = await executeStep ( workflow , execution , step , stepInput , executionData . abortController ?. signal ) ;
449+
450+ // Add step result to messages array for next step and capture meaningful output
451+ let meaningfulOutput = '' ;
452+ if ( stepResult . success && stepResult . result ) {
453+ // First, try to extract meaningful content for both buffer and display
454+ meaningfulOutput = extractMeaningfulContent ( stepResult . result ) ;
455+
456+ // If we found meaningful content, use it for the buffer string
457+ if ( meaningfulOutput ) {
458+ // Create a HumanMessage with the meaningful content for the next step
459+ stepMessages . push ( new HumanMessage ( `Step "${ step . name } " result: ${ meaningfulOutput } ` ) ) ;
460+
461+ logger . debug (
462+ `[WorkflowExecutor] Added meaningful step result to message chain: ${ meaningfulOutput . substring ( 0 , 200 ) } ...` ,
463+ ) ;
464+ } else {
465+ // Fallback to raw result if no meaningful content can be extracted
466+ let resultText = '' ;
467+ if ( typeof stepResult . result === 'string' ) {
468+ resultText = stepResult . result ;
469+ meaningfulOutput = resultText ;
470+ } else {
471+ resultText = JSON . stringify ( stepResult . result , null , 2 ) ;
472+ meaningfulOutput = resultText ;
473+ }
474+
475+ // Create a HumanMessage with the fallback result for the next step
476+ stepMessages . push ( new HumanMessage ( `Step "${ step . name } " result: ${ resultText } ` ) ) ;
477+
478+ logger . debug (
479+ `[WorkflowExecutor] Added fallback step result to message chain: ${ resultText . substring ( 0 , 200 ) } ...` ,
480+ ) ;
481+ }
482+ }
308483
309484 // Update step status and results in metadata
310485 if ( stepMetadata ) {
311486 stepMetadata . status = stepResult . success ? 'completed' : 'failed' ;
312487 stepMetadata . endTime = new Date ( ) ;
313488
314- // Capture step output and error
315- if ( stepResult . result && typeof stepResult . result === 'string' ) {
489+ // Store meaningful output instead of technical metadata
490+ if ( meaningfulOutput ) {
491+ stepMetadata . output = meaningfulOutput ;
492+ } else if ( stepResult . result && typeof stepResult . result === 'string' ) {
316493 stepMetadata . output = stepResult . result ;
317494 } else if ( stepResult . result && typeof stepResult . result === 'object' ) {
495+ // Fallback to technical metadata if no meaningful content can be extracted
318496 stepMetadata . output = JSON . stringify ( stepResult . result , null , 2 ) ;
319497 }
320498
@@ -377,6 +555,26 @@ class WorkflowExecutor {
377555 }
378556
379557 executionResult . result = accumulatedStepResults ;
558+
559+ // Create a final summary using the buffer string approach
560+ if ( stepMessages . length > 0 ) {
561+ const finalSummary = getBufferString ( stepMessages ) ;
562+ executionResult . finalOutput = finalSummary ;
563+
564+ // Update execution record with final summary
565+ await updateSchedulerExecution ( execution . id , execution . user , {
566+ status : executionResult . success ? 'completed' : 'failed' ,
567+ endTime : new Date ( ) ,
568+ output : finalSummary , // Store the final buffer string as overall output
569+ } ) ;
570+ } else {
571+ // Update execution record without buffer string
572+ await updateSchedulerExecution ( execution . id , execution . user , {
573+ status : executionResult . success ? 'completed' : 'failed' ,
574+ endTime : new Date ( ) ,
575+ } ) ;
576+ }
577+
380578 return executionResult ;
381579 }
382580
0 commit comments