Skip to content

fix(typescript): stop streamEvents() sending the current query twice - #2653

Closed
BlueX888 wants to merge 2 commits into
mcp-use:mainfrom
BlueX888:fix/streamevents-duplicate-query
Closed

BlueX888 wants to merge 2 commits into
mcp-use:mainfrom
BlueX888:fix/streamevents-duplicate-query

Conversation

@BlueX888

@BlueX888 BlueX888 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Where did you find this issue?

Found by code review while comparing how run()/stream() and streamEvents() assemble agent inputs in the LangChain MCPAgent (libraries/typescript/packages/agent/src/agents/mcp_agent_langchain.ts). With memoryEnabled: true (the default) and no externalHistory, streamEvents() sends the current user query to the model twice on every call. I did not find an existing issue tracking it.

Language / Project Scope

  • TypeScript
  • Python
  • Documentation only
  • CI/CD or tooling

Changes

streamEvents() pushed the user query into conversationHistory before taking the history snapshot for the run, then appended the query again as the trailing HumanMessage of the inputs — so the current turn reached the model twice. The addToHistory() call now runs after the inputs are assembled, so inputs = [...priorHistory, new HumanMessage(query)] and the query is stored once for the next turn.

Implementation Details

  1. packages/agent/src/agents/mcp_agent_langchain.ts (streamEvents()): moved the if (this.memoryEnabled) { this.addToHistory(new HumanMessage({ content: query })) } block from before const historyToUse = history ?? this.conversationHistory to after the inputs array is built. Before: turn 1 sent [human("hello"), human("hello")], turn 2 sent [hello, ai(answer), world, world].
  2. run()/stream() in the same file already snapshot the history first and only record messages afterwards (accumulatedMessages = [...langchainHistory, new HumanMessage(query)], with history updated after execution); streamEvents() now matches that behavior.
  3. No public API change; no dependencies added or modified.

TypeScript Checklist

Packages Modified

  • tests
  • Source change is in @mcp-use/agent (packages/agent/src/agents/mcp_agent_langchain.ts; the template has no agent checkbox).

Pre-commit Checklist

  • Ran pnpm lint:fix — not needed: pnpm lint reports no issues in the changed files (the 41 problems it reports are pre-existing on main, all in packages/cli/src/**)
  • Ran pnpm format — pnpm format:check passes
  • Ran pnpm build and build succeeds without errors
  • Ran pnpm changeset to create a changeset
  • Added or updated tests — packages/agent/tests/stream_events_duplicate_query.test.ts
  • Updated documentation in docs/ — not needed (bugfix only)

Example Usage (Before)

const agent = new MCPAgent({ llm, connectors, memoryEnabled: true }); // memoryEnabled defaults to true
for await (const event of agent.streamEvents({ prompt: "hello" })) { /* ... */ }
// executor receives messages: [human("hello"), human("hello")]

Example Usage (After)

const agent = new MCPAgent({ llm, connectors, memoryEnabled: true });
for await (const event of agent.streamEvents({ prompt: "hello" })) { /* ... */ }
// executor receives messages: [human("hello")]
// next turn: [human("hello"), ai("answer"), human("world")]

Documentation Updates

None — bugfix only, no public API change.

Testing

  • Added packages/agent/tests/stream_events_duplicate_query.test.ts: drives two consecutive streamEvents() turns against a mocked agent executor and asserts the executor receives the current query exactly once per turn (turn 1: [human("hello")]; turn 2: [human("hello"), ai("answer-1"), human("world")]).
  • On unpatched main the new test fails with AssertionError: expected 2 to be 1 (query counted twice in turn 1 inputs); with the fix it passes.
  • pnpm --filter @mcp-use/agent test:unit — 20 test files, 149 passed, 1 skipped (skip is pre-existing).
  • pnpm format:check — passes.
  • pnpm lint — no new issues; the 41 reported problems exist identically on main and are all in packages/cli/src/**.
  • pnpm build — succeeds.

Backwards Compatibility

Backwards compatible. The only observable change is that the current user query is no longer duplicated in the prompt sent to the model on each streamEvents() call; conversation memory contents are unchanged. Callers passing externalHistory are unaffected (external history is used instead of memory and never received the query).

Related Issues

No existing issue found in the tracker for this bug; happy to link one if a maintainer prefers an issue first.


Summary by cubic

Fixes MCPAgent.streamEvents() sending the current user query to the model twice on every call when memory is enabled (the default) and no externalHistory is provided. The query was pushed into conversationHistory before the history snapshot was taken and then appended again as the trailing HumanMessage; it is now recorded only after the inputs are assembled, matching run() and stream().

The addToHistory() call is now deferred until after the stream completes, so a stream that throws leaves no orphaned user message in conversation memory for the next turn.

Bug Fixes

  • Adds regression tests covering two consecutive streamEvents() turns and a failing stream.
  • Backwards compatible; conversation memory contents are unchanged.

Written for commit 2e0e2d5. Summary will update on new commits.

View guided diff

MCPAgent.streamEvents() pushed the user query into conversationHistory
before taking the history snapshot for the run, then appended the query
again as the trailing HumanMessage of the inputs. With memory enabled
and no external history the model received the current turn duplicated
on every call. run()/stream() take the snapshot first and only record
the query afterwards, so streamEvents() now does the same.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libraries/typescript/packages/agent/src/agents/mcp_agent_langchain.ts">

<violation number="1" location="libraries/typescript/packages/agent/src/agents/mcp_agent_langchain.ts:1872">
P3: The moved block still commits the user query to `conversationHistory` before the stream executes, so a stream that throws leaves an orphaned HumanMessage (no matching AI response) in memory, and the next turn feeds that unanswered query to the model. This diverges from the run()/stream() behavior the PR aims to mirror: there, all new messages are committed only after execution completes successfully (step 4, `accumulatedMessages.slice(langchainHistory.length)`). To fully mirror that and stay consistent on errors, defer the `addToHistory(new HumanMessage(...))` until after the event loop completes, alongside the existing `addToHistory(new AIMessage(finalResponse))` at the end of the function.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// Add user message to history if memory enabled
if (this.memoryEnabled) {
logger.debug(`🔄 Adding user message to history: ${display_query}`);
this.addToHistory(new HumanMessage({ content: query }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The moved block still commits the user query to conversationHistory before the stream executes, so a stream that throws leaves an orphaned HumanMessage (no matching AI response) in memory, and the next turn feeds that unanswered query to the model. This diverges from the run()/stream() behavior the PR aims to mirror: there, all new messages are committed only after execution completes successfully (step 4, accumulatedMessages.slice(langchainHistory.length)). To fully mirror that and stay consistent on errors, defer the addToHistory(new HumanMessage(...)) until after the event loop completes, alongside the existing addToHistory(new AIMessage(finalResponse)) at the end of the function.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At libraries/typescript/packages/agent/src/agents/mcp_agent_langchain.ts, line 1872:

<comment>The moved block still commits the user query to `conversationHistory` before the stream executes, so a stream that throws leaves an orphaned HumanMessage (no matching AI response) in memory, and the next turn feeds that unanswered query to the model. This diverges from the run()/stream() behavior the PR aims to mirror: there, all new messages are committed only after execution completes successfully (step 4, `accumulatedMessages.slice(langchainHistory.length)`). To fully mirror that and stay consistent on errors, defer the `addToHistory(new HumanMessage(...))` until after the event loop completes, alongside the existing `addToHistory(new AIMessage(finalResponse))` at the end of the function.</comment>

<file context>
@@ -1872,6 +1866,12 @@ export class MCPAgent {
+      // Add user message to history if memory enabled
+      if (this.memoryEnabled) {
+        logger.debug(`🔄 Adding user message to history: ${display_query}`);
+        this.addToHistory(new HumanMessage({ content: query }));
+      }
+
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Moving the history insertion below the input snapshot fixes the duplicate-query request, but the insertion still happens before streamEvents() completes. If the stream throws, memory retains the new HumanMessage without the corresponding AI message. Deferring the memory writes until successful completion, and covering the failing-stream path, would preserve both the single-query invariant and consistent history on errors. The current branch has not addressed this follow-up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2e0e2d5: the addToHistory(new HumanMessage(query)) in streamEvents() now runs only after the event loop completes, so a stream that throws leaves conversationHistory untouched, matching run()/stream() which commit new messages to memory only after successful execution. Added a regression test that drives a failing stream and asserts no orphaned HumanMessage remains (packages/agent/tests/stream_events_duplicate_query.test.ts). pnpm --filter @mcp-use/agent test:unit passes: 20 files, 150 passed, 1 pre-existing skip.

streamEvents() committed the user query to conversationHistory before the
event loop started, so a stream that threw left an orphaned HumanMessage
(with no matching AI response) that the next turn fed back to the model.
Defer the addToHistory() call until after the event loop completes,
mirroring run()/stream() which commit new messages to memory only after
successful execution.
@khandrew1

Copy link
Copy Markdown
Contributor

Hey, thanks for the PR. We're no longer supporting @mcp-use/agent, so we won't be merging changes to it. Our focus is the TypeScript server and client SDKs. If you're running the agent SDK in a production project and this is blocking you, let us know and we can take another look. Otherwise I'm going to close this out.

@khandrew1 khandrew1 closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants