app.stream showing duplicate Human Messages during execution #44

Closed
opened 2026-02-15 17:14:55 -05:00 by yindo · 3 comments
Owner

Originally created by @justinlevi on GitHub (Jul 8, 2024).

I'm working through a very simple todolist example:

My AgentState

import { BaseMessage } from '@langchain/core/messages';

export const state = {
  messages: {
    value: (x: BaseMessage[], y: BaseMessage[]) => {
      return x.concat(y);
    },
    default: () => [],
  },
};

My Graph looks like this:

const workflow = new StateGraph<AgentState>({
      channels: state,
    })
      .addNode('AGENT', this.agentNode.run.bind(this.agentNode))
      .addNode('CALL_TOOL', this.callToolNode)
      .addEdge(START, 'AGENT')
      .addConditionalEdges('AGENT', this.shouldContinue, {
        CONTINUE: 'CALL_TOOL',
        END,
      })
      .addEdge('CALL_TOOL', 'AGENT');

    return workflow.compile();
    ````
    
    If I log during the stream 
    ```
    ...
    for await (const output of await this.app.stream(inputs)) {
      for (const [key, value] of Object.entries(output)) {
        const typedValue = value as { messages: BaseMessage[] };

        // Log the messages
        this.logMessages(key, typedValue.messages);

        result = typedValue;
      }
    }
    

I get the following output


     --- Messages from AGENT node ---
[Human Message 1]:
Content: Can you provide me with all the details of the todos I have added?
---
[AI Message 2]:
Content: 
Tool Calls:
  Tool 1:
    Name: fetch_todos
    Arguments: {}
---

--- Messages from CALL_TOOL node ---
[AI Message 1]:
Content: [{"id":26,"title":"Talk with George later this afternoon","completed":false,"createdAt":"2024-07-01T18:12:11.608Z","updatedAt":"2024-07-01T18:12:11.608Z","parentId":null,"urgency":null,"urgencyCertaintyScore":null,"importance":null,"importanceCertaintyScore":null,"deadline":null,"deadlineCertaintyScore":null,"estimatedTime":null,"estimatedTimeCertaintyScore":null,"energyLevel":null,"energyLevelCertaintyScore":null,"context":null,"contextCertaintyScore":null,"priority":"A","priorityCertaintyScore":null,"impact":null,"impactCertaintyScore":null,"effort":null,"effortCertaintyScore":null,"category":null,"categoryCertaintyScore":null,"value":null,"valueCertaintyScore":null,"confidence":null,"confidenceCertaintyScore":null,"reach":null,"reachCertaintyScore":null,"assignee":null,"assigneeCertaintyScore":null,"status":null,"statusCertaintyScore":null,"project":null,"projectCertaintyScore":null,"recurrence":null,"recurrenceCertaintyScore":null,"timeSpent":null,"timeSpentCertaintyScore":null,"customerSatisfaction":null,"customerSatisfactionCertaintyScore":null,"quadrant":null,"quadrantCertaintyScore":null,"sequence":null,"sequenceCertaintyScore":null,"children":[],"tags":[{"id":5,"name":"run"},{"id":9,"name":"PHone"},{"id":10,"name":"Jump rope"}]}]
---

--- Messages from AGENT node ---
[Human Message 1]:
Content: Can you provide me with all the details of the todos I have added?
---
[Human Message 2]:
Content: Can you provide me with all the details of the todos I have added?
---
[AI Message 3]:
Content: 
Tool Calls:
  Tool 1:
    Name: fetch_todos
    Arguments: {}
---
[AI Message 4]:
Content: [{"id":26,"title":"Talk with George later this afternoon","completed":false,"createdAt":"2024-07-01T18:12:11.608Z","updatedAt":"2024-07-01T18:12:11.608Z","parentId":null,"urgency":null,"urgencyCertaintyScore":null,"importance":null,"importanceCertaintyScore":null,"deadline":null,"deadlineCertaintyScore":null,"estimatedTime":null,"estimatedTimeCertaintyScore":null,"energyLevel":null,"energyLevelCertaintyScore":null,"context":null,"contextCertaintyScore":null,"priority":"A","priorityCertaintyScore":null,"impact":null,"impactCertaintyScore":null,"effort":null,"effortCertaintyScore":null,"category":null,"categoryCertaintyScore":null,"value":null,"valueCertaintyScore":null,"confidence":null,"confidenceCertaintyScore":null,"reach":null,"reachCertaintyScore":null,"assignee":null,"assigneeCertaintyScore":null,"status":null,"statusCertaintyScore":null,"project":null,"projectCertaintyScore":null,"recurrence":null,"recurrenceCertaintyScore":null,"timeSpent":null,"timeSpentCertaintyScore":null,"customerSatisfaction":null,"customerSatisfactionCertaintyScore":null,"quadrant":null,"quadrantCertaintyScore":null,"sequence":null,"sequenceCertaintyScore":null,"children":[],"tags":[{"id":5,"name":"run"},{"id":9,"name":"PHone"},{"id":10,"name":"Jump rope"}]}]
---
[AI Message 5]:
Content: Here are the details of the todos you have added:

### Todo 1
- **Title:** Talk with George later this afternoon
- **Completed:** No
- **Created At:** 2024-07-01T18:12:11.608Z
- **Updated At:** 2024-07-01T18:12:11.608Z
- **Priority:** A
- **Tags:** 
  - run
  - PHone
  - Jump rope

If you need any further details or actions, feel free to ask!
---

Hack fix

I can bypass the issue by adjusting my state to:

export const state = {
 messages: {
   value: (x: BaseMessage[], y: BaseMessage[]) => {
     if (x.length === 1) {
       return y;
     }
     return x.concat(y);
   },
   default: () => [],
 },
};

or by deduping the messages, via something like this:

value: (x: BaseMessage[], y: BaseMessage[]) => {
 const combined = x.concat(y);
 return combined.filter((message, index, self) =>
   index === self.findIndex((m) => m.content === message.content)
 );
},

I'm just wondering if there is a better way to avoid this or if it's a bug?
https://github.com/langchain-ai/langgraphjs/blob/main/langgraph/src/channels/binop.ts

Originally created by @justinlevi on GitHub (Jul 8, 2024). I'm working through a very simple todolist example: My AgentState ``` import { BaseMessage } from '@langchain/core/messages'; export const state = { messages: { value: (x: BaseMessage[], y: BaseMessage[]) => { return x.concat(y); }, default: () => [], }, }; ``` My Graph looks like this: ``` const workflow = new StateGraph<AgentState>({ channels: state, }) .addNode('AGENT', this.agentNode.run.bind(this.agentNode)) .addNode('CALL_TOOL', this.callToolNode) .addEdge(START, 'AGENT') .addConditionalEdges('AGENT', this.shouldContinue, { CONTINUE: 'CALL_TOOL', END, }) .addEdge('CALL_TOOL', 'AGENT'); return workflow.compile(); ```` If I log during the stream ``` ... for await (const output of await this.app.stream(inputs)) { for (const [key, value] of Object.entries(output)) { const typedValue = value as { messages: BaseMessage[] }; // Log the messages this.logMessages(key, typedValue.messages); result = typedValue; } } ``` I get the following output ``` --- Messages from AGENT node --- [Human Message 1]: Content: Can you provide me with all the details of the todos I have added? --- [AI Message 2]: Content: Tool Calls: Tool 1: Name: fetch_todos Arguments: {} --- --- Messages from CALL_TOOL node --- [AI Message 1]: Content: [{"id":26,"title":"Talk with George later this afternoon","completed":false,"createdAt":"2024-07-01T18:12:11.608Z","updatedAt":"2024-07-01T18:12:11.608Z","parentId":null,"urgency":null,"urgencyCertaintyScore":null,"importance":null,"importanceCertaintyScore":null,"deadline":null,"deadlineCertaintyScore":null,"estimatedTime":null,"estimatedTimeCertaintyScore":null,"energyLevel":null,"energyLevelCertaintyScore":null,"context":null,"contextCertaintyScore":null,"priority":"A","priorityCertaintyScore":null,"impact":null,"impactCertaintyScore":null,"effort":null,"effortCertaintyScore":null,"category":null,"categoryCertaintyScore":null,"value":null,"valueCertaintyScore":null,"confidence":null,"confidenceCertaintyScore":null,"reach":null,"reachCertaintyScore":null,"assignee":null,"assigneeCertaintyScore":null,"status":null,"statusCertaintyScore":null,"project":null,"projectCertaintyScore":null,"recurrence":null,"recurrenceCertaintyScore":null,"timeSpent":null,"timeSpentCertaintyScore":null,"customerSatisfaction":null,"customerSatisfactionCertaintyScore":null,"quadrant":null,"quadrantCertaintyScore":null,"sequence":null,"sequenceCertaintyScore":null,"children":[],"tags":[{"id":5,"name":"run"},{"id":9,"name":"PHone"},{"id":10,"name":"Jump rope"}]}] --- --- Messages from AGENT node --- [Human Message 1]: Content: Can you provide me with all the details of the todos I have added? --- [Human Message 2]: Content: Can you provide me with all the details of the todos I have added? --- [AI Message 3]: Content: Tool Calls: Tool 1: Name: fetch_todos Arguments: {} --- [AI Message 4]: Content: [{"id":26,"title":"Talk with George later this afternoon","completed":false,"createdAt":"2024-07-01T18:12:11.608Z","updatedAt":"2024-07-01T18:12:11.608Z","parentId":null,"urgency":null,"urgencyCertaintyScore":null,"importance":null,"importanceCertaintyScore":null,"deadline":null,"deadlineCertaintyScore":null,"estimatedTime":null,"estimatedTimeCertaintyScore":null,"energyLevel":null,"energyLevelCertaintyScore":null,"context":null,"contextCertaintyScore":null,"priority":"A","priorityCertaintyScore":null,"impact":null,"impactCertaintyScore":null,"effort":null,"effortCertaintyScore":null,"category":null,"categoryCertaintyScore":null,"value":null,"valueCertaintyScore":null,"confidence":null,"confidenceCertaintyScore":null,"reach":null,"reachCertaintyScore":null,"assignee":null,"assigneeCertaintyScore":null,"status":null,"statusCertaintyScore":null,"project":null,"projectCertaintyScore":null,"recurrence":null,"recurrenceCertaintyScore":null,"timeSpent":null,"timeSpentCertaintyScore":null,"customerSatisfaction":null,"customerSatisfactionCertaintyScore":null,"quadrant":null,"quadrantCertaintyScore":null,"sequence":null,"sequenceCertaintyScore":null,"children":[],"tags":[{"id":5,"name":"run"},{"id":9,"name":"PHone"},{"id":10,"name":"Jump rope"}]}] --- [AI Message 5]: Content: Here are the details of the todos you have added: ### Todo 1 - **Title:** Talk with George later this afternoon - **Completed:** No - **Created At:** 2024-07-01T18:12:11.608Z - **Updated At:** 2024-07-01T18:12:11.608Z - **Priority:** A - **Tags:** - run - PHone - Jump rope If you need any further details or actions, feel free to ask! --- ``` ## Hack fix I can bypass the issue by adjusting my state to: ``` export const state = { messages: { value: (x: BaseMessage[], y: BaseMessage[]) => { if (x.length === 1) { return y; } return x.concat(y); }, default: () => [], }, }; ``` or by deduping the messages, via something like this: ``` value: (x: BaseMessage[], y: BaseMessage[]) => { const combined = x.concat(y); return combined.filter((message, index, self) => index === self.findIndex((m) => m.content === message.content) ); }, ``` I'm just wondering if there is a better way to avoid this or if it's a bug? https://github.com/langchain-ai/langgraphjs/blob/main/langgraph/src/channels/binop.ts
yindo closed this issue 2026-02-15 17:14:55 -05:00
Author
Owner

@vbarda commented on GitHub (Jul 8, 2024):

@justinlevi it sounds like you're returning full list of messages from agent instead of just new messages (i.e. full history of messages, including original human message, instead of just AIMessage). this is not necessarily wrong, but you do need to handle this when defining your state object

so you're on the right track re: adjusting the state definition: the function you're specifying in value is called "reducer" -- it tells langgraph how it should update the state for the messages key. you probably want to change it to reduce the list of messages based on their IDs. something like this:

function addMessages(left, right) {
  let leftCopy = Array.isArray(left) ? left.slice() : [left];
  let rightCopy = Array.isArray(right) ? right.slice() : [right];

  // Assign missing ids
  leftCopy.forEach(m => {
      if (!m.id) {
          m.id = uuidv4();
      }
  });
  rightCopy.forEach(m => {
      if (!m.id) {
          m.id = uuidv4();
      }
  });

  let leftIdxById = {};
  leftCopy.forEach((m, i) => {
      leftIdxById[m.id] = i;
  });

  let merged = leftCopy.slice();
  rightCopy.forEach(m => {
      let existingIdx = leftIdxById[m.id];
      if (existingIdx !== undefined) {
          merged[existingIdx] = m;
      } else {
          merged.push(m);
      }
  });

  return merged;
}
@vbarda commented on GitHub (Jul 8, 2024): @justinlevi it sounds like you're returning full list of messages from agent instead of just new messages (i.e. full history of messages, including original human message, instead of just `AIMessage`). this is not necessarily wrong, but you do need to handle this when defining your state object so you're on the right track re: adjusting the state definition: the function you're specifying in value is called "reducer" -- it tells langgraph how it should update the state for the `messages` key. you probably want to change it to reduce the list of messages based on their IDs. something like this: ```js function addMessages(left, right) { let leftCopy = Array.isArray(left) ? left.slice() : [left]; let rightCopy = Array.isArray(right) ? right.slice() : [right]; // Assign missing ids leftCopy.forEach(m => { if (!m.id) { m.id = uuidv4(); } }); rightCopy.forEach(m => { if (!m.id) { m.id = uuidv4(); } }); let leftIdxById = {}; leftCopy.forEach((m, i) => { leftIdxById[m.id] = i; }); let merged = leftCopy.slice(); rightCopy.forEach(m => { let existingIdx = leftIdxById[m.id]; if (existingIdx !== undefined) { merged[existingIdx] = m; } else { merged.push(m); } }); return merged; } ```
Author
Owner

@justinlevi commented on GitHub (Jul 9, 2024):

@vbarda Thanks so much for the explanation. Makes perfect sense.

Was able to get my POC demo working along with visualizing the graph flow
to-do-list

@justinlevi commented on GitHub (Jul 9, 2024): @vbarda Thanks so much for the explanation. Makes perfect sense. Was able to get my POC demo working along with visualizing the graph flow ![to-do-list](https://github.com/langchain-ai/langgraphjs/assets/1117989/61d4f875-017d-466d-937b-6be76d45128e)
Author
Owner

@vbarda commented on GitHub (Jul 9, 2024):

awesome, glad i could help! looks great!

@vbarda commented on GitHub (Jul 9, 2024): awesome, glad i could help! looks great!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#44