11import { addLangChainErrorFields } from "../errors/index.js" ;
22import { SerializedConstructor } from "../load/serializable.js" ;
33import { _isToolCall } from "../tools/utils.js" ;
4+ import { parsePartialJson } from "../utils/json.js" ;
45import { AIMessage , AIMessageChunk , AIMessageChunkFields } from "./ai.js" ;
56import {
67 BaseMessageLike ,
@@ -20,7 +21,13 @@ import {
2021import { HumanMessage , HumanMessageChunk } from "./human.js" ;
2122import { RemoveMessage } from "./modifier.js" ;
2223import { SystemMessage , SystemMessageChunk } from "./system.js" ;
23- import { ToolCall , ToolMessage , ToolMessageFields } from "./tool.js" ;
24+ import {
25+ InvalidToolCall ,
26+ ToolCall ,
27+ ToolCallChunk ,
28+ ToolMessage ,
29+ ToolMessageFields ,
30+ } from "./tool.js" ;
2431
2532export type $Expand < T > = T extends infer U ? { [ K in keyof U ] : U [ K ] } : never ;
2633
@@ -445,3 +452,107 @@ export function convertToChunk(message: BaseMessage) {
445452 throw new Error ( "Unknown message type." ) ;
446453 }
447454}
455+
456+ /**
457+ * Collapses an array of tool call chunks into complete tool calls.
458+ *
459+ * This function groups tool call chunks by their id and/or index, then attempts to
460+ * parse and validate the accumulated arguments for each group. Successfully parsed
461+ * tool calls are returned as valid `ToolCall` objects, while malformed ones are
462+ * returned as `InvalidToolCall` objects.
463+ *
464+ * @param chunks - An array of `ToolCallChunk` objects to collapse
465+ * @returns An object containing:
466+ * - `tool_call_chunks`: The original input chunks
467+ * - `tool_calls`: An array of successfully parsed and validated tool calls
468+ * - `invalid_tool_calls`: An array of tool calls that failed parsing or validation
469+ *
470+ * @remarks
471+ * Chunks are grouped using the following matching logic:
472+ * - If a chunk has both an id and index, it matches chunks with the same id and index
473+ * - If a chunk has only an id, it matches chunks with the same id
474+ * - If a chunk has only an index, it matches chunks with the same index
475+ *
476+ * For each group, the function:
477+ * 1. Concatenates all `args` strings from the chunks
478+ * 2. Attempts to parse the concatenated string as JSON
479+ * 3. Validates that the result is a non-null object with a valid id
480+ * 4. Creates either a `ToolCall` (if valid) or `InvalidToolCall` (if invalid)
481+ */
482+ export function collapseToolCallChunks ( chunks : ToolCallChunk [ ] ) : {
483+ tool_call_chunks : ToolCallChunk [ ] ;
484+ tool_calls : ToolCall [ ] ;
485+ invalid_tool_calls : InvalidToolCall [ ] ;
486+ } {
487+ const groupedToolCallChunks = chunks . reduce ( ( acc , chunk ) => {
488+ const matchedChunkIndex = acc . findIndex ( ( [ match ] ) => {
489+ // If chunk has an id and index, match if both are present
490+ if (
491+ "id" in chunk &&
492+ chunk . id &&
493+ "index" in chunk &&
494+ chunk . index !== undefined
495+ ) {
496+ return chunk . id === match . id && chunk . index === match . index ;
497+ }
498+ // If chunk has an id, we match on id
499+ if ( "id" in chunk && chunk . id ) {
500+ return chunk . id === match . id ;
501+ }
502+ // If chunk has an index, we match on index
503+ if ( "index" in chunk && chunk . index !== undefined ) {
504+ return chunk . index === match . index ;
505+ }
506+ return false ;
507+ } ) ;
508+ if ( matchedChunkIndex !== - 1 ) {
509+ acc [ matchedChunkIndex ] . push ( chunk ) ;
510+ } else {
511+ acc . push ( [ chunk ] ) ;
512+ }
513+ return acc ;
514+ } , [ ] as ToolCallChunk [ ] [ ] ) ;
515+
516+ const toolCalls : ToolCall [ ] = [ ] ;
517+ const invalidToolCalls : InvalidToolCall [ ] = [ ] ;
518+ for ( const chunks of groupedToolCallChunks ) {
519+ let parsedArgs : Record < string , unknown > | null = null ;
520+ const name = chunks [ 0 ] ?. name ?? "" ;
521+ const joinedArgs = chunks
522+ . map ( ( c ) => c . args || "" )
523+ . join ( "" )
524+ . trim ( ) ;
525+ const argsStr = joinedArgs . length ? joinedArgs : "{}" ;
526+ const id = chunks [ 0 ] ?. id ;
527+ try {
528+ parsedArgs = parsePartialJson ( argsStr ) ;
529+ if (
530+ ! id ||
531+ parsedArgs === null ||
532+ typeof parsedArgs !== "object" ||
533+ Array . isArray ( parsedArgs )
534+ ) {
535+ throw new Error ( "Malformed tool call chunk args." ) ;
536+ }
537+ toolCalls . push ( {
538+ name,
539+ args : parsedArgs ,
540+ id,
541+ type : "tool_call" ,
542+ } ) ;
543+ } catch {
544+ invalidToolCalls . push ( {
545+ name,
546+ args : argsStr ,
547+ id,
548+ error : "Malformed args." ,
549+ type : "invalid_tool_call" ,
550+ } ) ;
551+ }
552+ }
553+ return {
554+ tool_call_chunks : chunks ,
555+ tool_calls : toolCalls ,
556+ invalid_tool_calls : invalidToolCalls ,
557+ } ;
558+ }
0 commit comments