@@ -14,7 +14,7 @@ import { getBlock } from '@/blocks'
1414import { db } from '@/db'
1515import { chat , environment as envTable , userStats , workflow } from '@/db/schema'
1616import { Executor } from '@/executor'
17- import type { BlockLog } from '@/executor/types'
17+ import type { BlockLog , ExecutionResult } from '@/executor/types'
1818import { Serializer } from '@/serializer'
1919import { mergeSubblockState } from '@/stores/workflows/server-utils'
2020import type { WorkflowState } from '@/stores/workflows/workflow/types'
@@ -549,6 +549,7 @@ export async function executeWorkflowForChat(
549549 async start ( controller ) {
550550 const encoder = new TextEncoder ( )
551551 const streamedContent = new Map < string , string > ( )
552+ const streamedBlocks = new Set < string > ( ) // Track which blocks have started streaming
552553
553554 const onStream = async ( streamingExecution : any ) : Promise < void > => {
554555 if ( ! streamingExecution . stream ) return
@@ -557,6 +558,15 @@ export async function executeWorkflowForChat(
557558 const reader = streamingExecution . stream . getReader ( )
558559 if ( blockId ) {
559560 streamedContent . set ( blockId , '' )
561+
562+ // Add separator if this is not the first block to stream
563+ if ( streamedBlocks . size > 0 ) {
564+ // Send separator before the new block starts
565+ controller . enqueue (
566+ encoder . encode ( `data: ${ JSON . stringify ( { blockId, chunk : '\n\n' } ) } \n\n` )
567+ )
568+ }
569+ streamedBlocks . add ( blockId )
560570 }
561571 try {
562572 while ( true ) {
@@ -615,25 +625,117 @@ export async function executeWorkflowForChat(
615625 throw error
616626 }
617627
618- if ( result && 'success' in result ) {
619- // Update streamed content and apply tokenization
620- if ( result . logs ) {
621- result . logs . forEach ( ( log : BlockLog ) => {
622- if ( streamedContent . has ( log . blockId ) ) {
623- const content = streamedContent . get ( log . blockId )
624- if ( log . output ) {
625- log . output . content = content
628+ // Handle both ExecutionResult and StreamingExecution types
629+ const executionResult =
630+ result && typeof result === 'object' && 'execution' in result
631+ ? ( result . execution as ExecutionResult )
632+ : ( result as ExecutionResult )
633+
634+ if ( executionResult ?. logs ) {
635+ // Update streamed content and apply tokenization - process regardless of overall success
636+ // This ensures partial successes (some agents succeed, some fail) still return results
637+
638+ // Add newlines between different agent outputs for better readability
639+ const processedOutputs = new Set < string > ( )
640+ executionResult . logs . forEach ( ( log : BlockLog ) => {
641+ if ( streamedContent . has ( log . blockId ) ) {
642+ const content = streamedContent . get ( log . blockId )
643+ if ( log . output && content ) {
644+ // Add newline separation between different outputs (but not before the first one)
645+ const separator = processedOutputs . size > 0 ? '\n\n' : ''
646+ log . output . content = separator + content
647+ processedOutputs . add ( log . blockId )
648+ }
649+ }
650+ } )
651+
652+ // Also process non-streamed outputs from selected blocks (like function blocks)
653+ // This uses the same logic as the chat panel to ensure identical behavior
654+ const nonStreamingLogs = executionResult . logs . filter (
655+ ( log : BlockLog ) => ! streamedContent . has ( log . blockId )
656+ )
657+
658+ // Extract the exact same functions used by the chat panel
659+ const extractBlockIdFromOutputId = ( outputId : string ) : string => {
660+ return outputId . includes ( '_' ) ? outputId . split ( '_' ) [ 0 ] : outputId . split ( '.' ) [ 0 ]
661+ }
662+
663+ const extractPathFromOutputId = ( outputId : string , blockId : string ) : string => {
664+ return outputId . substring ( blockId . length + 1 )
665+ }
666+
667+ const parseOutputContentSafely = ( output : any ) : any => {
668+ if ( ! output ?. content ) {
669+ return output
670+ }
671+
672+ if ( typeof output . content === 'string' ) {
673+ try {
674+ return JSON . parse ( output . content )
675+ } catch ( e ) {
676+ // Fallback to original structure if parsing fails
677+ return output
678+ }
679+ }
680+
681+ return output
682+ }
683+
684+ // Filter outputs that have matching logs (exactly like chat panel)
685+ const outputsToRender = selectedOutputIds . filter ( ( outputId ) => {
686+ const blockIdForOutput = extractBlockIdFromOutputId ( outputId )
687+ return nonStreamingLogs . some ( ( log ) => log . blockId === blockIdForOutput )
688+ } )
689+
690+ // Process each selected output (exactly like chat panel)
691+ for ( const outputId of outputsToRender ) {
692+ const blockIdForOutput = extractBlockIdFromOutputId ( outputId )
693+ const path = extractPathFromOutputId ( outputId , blockIdForOutput )
694+ const log = nonStreamingLogs . find ( ( l ) => l . blockId === blockIdForOutput )
695+
696+ if ( log ) {
697+ let outputValue : any = log . output
698+
699+ if ( path ) {
700+ // Parse JSON content safely (exactly like chat panel)
701+ outputValue = parseOutputContentSafely ( outputValue )
702+
703+ const pathParts = path . split ( '.' )
704+ for ( const part of pathParts ) {
705+ if ( outputValue && typeof outputValue === 'object' && part in outputValue ) {
706+ outputValue = outputValue [ part ]
707+ } else {
708+ outputValue = undefined
709+ break
710+ }
626711 }
627712 }
628- } )
629713
630- // Process all logs for streaming tokenization
631- const processedCount = processStreamingBlockLogs ( result . logs , streamedContent )
632- logger . info ( `[CHAT-API] Processed ${ processedCount } blocks for streaming tokenization` )
714+ if ( outputValue !== undefined ) {
715+ // Add newline separation between different outputs
716+ const separator = processedOutputs . size > 0 ? '\n\n' : ''
717+
718+ // Format the output exactly like the chat panel
719+ const formattedOutput =
720+ typeof outputValue === 'string' ? outputValue : JSON . stringify ( outputValue , null , 2 )
721+
722+ // Update the log content
723+ if ( ! log . output . content ) {
724+ log . output . content = separator + formattedOutput
725+ } else {
726+ log . output . content = separator + formattedOutput
727+ }
728+ processedOutputs . add ( log . blockId )
729+ }
730+ }
633731 }
634732
635- const { traceSpans, totalDuration } = buildTraceSpans ( result )
636- const enrichedResult = { ...result , traceSpans, totalDuration }
733+ // Process all logs for streaming tokenization
734+ const processedCount = processStreamingBlockLogs ( executionResult . logs , streamedContent )
735+ logger . info ( `Processed ${ processedCount } blocks for streaming tokenization` )
736+
737+ const { traceSpans, totalDuration } = buildTraceSpans ( executionResult )
738+ const enrichedResult = { ...executionResult , traceSpans, totalDuration }
637739 if ( conversationId ) {
638740 if ( ! enrichedResult . metadata ) {
639741 enrichedResult . metadata = {
@@ -646,7 +748,7 @@ export async function executeWorkflowForChat(
646748 const executionId = uuidv4 ( )
647749 logger . debug ( `Generated execution ID for deployed chat: ${ executionId } ` )
648750
649- if ( result . success ) {
751+ if ( executionResult . success ) {
650752 try {
651753 await db
652754 . update ( userStats )
@@ -669,12 +771,12 @@ export async function executeWorkflowForChat(
669771 }
670772
671773 // Complete logging session (for both success and failure)
672- if ( result && 'success' in result ) {
673- const { traceSpans } = buildTraceSpans ( result )
774+ if ( executionResult ?. logs ) {
775+ const { traceSpans } = buildTraceSpans ( executionResult )
674776 await loggingSession . safeComplete ( {
675777 endedAt : new Date ( ) . toISOString ( ) ,
676- totalDurationMs : result . metadata ?. duration || 0 ,
677- finalOutput : result . output ,
778+ totalDurationMs : executionResult . metadata ?. duration || 0 ,
779+ finalOutput : executionResult . output ,
678780 traceSpans,
679781 } )
680782 }
0 commit comments