@@ -12,7 +12,6 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
1212 }
1313
1414 asyncEnd ( data ) {
15- // Exit early if disabled.
1615 if ( ! this . enabled ) {
1716 this . logger . debug ( '`ai_monitoring.enabled` is set to false, stream will not be instrumented.' )
1817 return
@@ -23,15 +22,16 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
2322 return
2423 }
2524
26- // Get context.
2725 const ctx = this . agent . tracer . getContext ( )
2826 const { transaction } = ctx
2927 if ( transaction ?. isActive ( ) !== true ) {
3028 return
3129 }
3230
33- // Extract data.
3431 const request = data ?. arguments ?. [ 0 ]
32+ // Requests via LangGraph API have the `messages` property with the
33+ // information we need, otherwise it just lives on the `request`
34+ // object directly.
3535 const userRequest = request ?. messages ? request . messages ?. [ 0 ] : request
3636 const params = data ?. arguments ?. [ 1 ] || { }
3737 const metadata = params ?. metadata ?? { }
@@ -41,7 +41,7 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
4141 // Note: as of 18.x `ReadableStream` is a global
4242 // eslint-disable-next-line n/no-unsupported-features/node-builtins
4343 if ( response instanceof ReadableStream ) {
44- this . wrapNextHandler ( { response, ctx, request : userRequest , metadata, tags } )
44+ this . instrumentStream ( { response, ctx, request : userRequest , metadata, tags } )
4545 } else {
4646 // Input error occurred which means a stream was not created.
4747 // Skip instrumenting streaming and create Llm Events from
@@ -67,22 +67,22 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
6767 * @param {object } params.metadata metadata for the call
6868 * @param {Array } params.tags tags for the call
6969 */
70- wrapNextHandler ( { ctx, response, request, metadata, tags } ) {
70+ instrumentStream ( { ctx, response, request, metadata, tags } ) {
7171 const self = this
7272 const orig = response . getReader
7373 response . getReader = function wrapedGetReader ( ) {
7474 const reader = orig . apply ( this , arguments )
7575 const origRead = reader . read
76- let content = ''
77- let langgraphMessages = [ ]
76+ const accumulator = { content : '' , langgraphMessages : [ ] }
7877 reader . read = async function wrappedRead ( ...args ) {
7978 try {
8079 const result = await origRead . apply ( this , args )
8180 if ( result ?. done ) {
8281 // only create Llm events when stream iteration is done
83- const responseMsgs = langgraphMessages . length > 0
84- ? langgraphMessages . filter ( ( msg ) => msg . constructor ?. name !== 'HumanMessage' )
85- : content
82+ const responseMsgs = self . getResponseMessages (
83+ accumulator . langgraphMessages ,
84+ accumulator . content
85+ )
8686 self . recordChatCompletionEvents ( {
8787 ctx,
8888 response : responseMsgs ,
@@ -92,29 +92,14 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
9292 } )
9393 } else {
9494 // Concat the streamed content
95- if ( result ?. value ?. messages || result ?. value ?. agent ?. messages ) {
96- // LangGraph case:
97- // The result.value.messages field contains all messages,
98- // request and response, and adds new events for the length
99- // of the stream. The last iteration will contain all messages
100- // in the stream so we can just re-assign it.
101- langgraphMessages = result ?. value ?. messages ?? result ?. value ?. agent ?. messages
102- } else if ( typeof result ?. value ?. content === 'string' ) {
103- // LangChain MessageChunk case
104- content += result . value . content
105- } else if ( typeof result ?. value === 'string' ) {
106- // Base LangChain case
107- content += result . value
108- } else if ( typeof result ?. value ?. [ 0 ] === 'string' ) {
109- // Array parser case
110- content += result . value [ 0 ]
111- }
95+ self . accumulateStreamContent ( result , accumulator )
11296 }
11397 return result
11498 } catch ( error ) {
115- const responseMsgs = langgraphMessages . length > 0
116- ? langgraphMessages . filter ( ( msg ) => msg . constructor ?. name !== 'HumanMessage' )
117- : content
99+ const responseMsgs = self . getResponseMessages (
100+ accumulator . langgraphMessages ,
101+ accumulator . content
102+ )
118103 self . recordChatCompletionEvents ( {
119104 ctx,
120105 request,
@@ -125,14 +110,55 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
125110 } )
126111 throw error
127112 } finally {
128- // update segment duration on every stream iteration to extend
129- // the timer
113+ // update segment duration on every stream
114+ // iteration to extend the timer
130115 ctx . segment . touch ( )
131116 }
132117 }
133118 return reader
134119 }
135120 }
121+
122+ /**
123+ * Accumulates streamed content from various LangChain/LangGraph result formats.
124+ *
125+ * @param {object } result the stream result chunk
126+ * @param {object } accumulator object containing content string and langgraphMessages array
127+ */
128+ accumulateStreamContent ( result , accumulator ) {
129+ if ( result ?. value ?. messages || result ?. value ?. agent ?. messages ) {
130+ // LangGraph case:
131+ // The result.value.messages field contains all messages,
132+ // request and response, and adds new events for the length
133+ // of the stream. The last iteration will contain all messages
134+ // in the stream, so we can just re-assign `langgraphMessages`.
135+ accumulator . langgraphMessages = result ?. value ?. messages ?? result ?. value ?. agent ?. messages
136+ } else if ( typeof result ?. value ?. content === 'string' ) {
137+ // LangChain MessageChunk case
138+ accumulator . content += result . value . content
139+ } else if ( typeof result ?. value === 'string' ) {
140+ // Base LangChain case
141+ accumulator . content += result . value
142+ } else if ( typeof result ?. value ?. [ 0 ] === 'string' ) {
143+ // LangChain array parser case
144+ accumulator . content += result . value [ 0 ]
145+ }
146+ }
147+
148+ /**
149+ * Extracts response messages from accumulated stream data.
150+ * For LangGraph, filters out HumanMessages.
151+ * For LangChain, returns concatenated content.
152+ *
153+ * @param {object[] } langgraphMessages accumulated LangGraph messages
154+ * @param {string } content accumulated LangChain content
155+ * @returns {object[]|string } the response messages
156+ */
157+ getResponseMessages ( langgraphMessages , content ) {
158+ return langgraphMessages . length > 0
159+ ? langgraphMessages . filter ( ( msg ) => msg . constructor ?. name !== 'HumanMessage' )
160+ : content
161+ }
136162}
137163
138164module . exports = LangchainRunnableStreamSubscriber
0 commit comments