Hi LangChain community,
Using @langchain/langgraph-sdk useStream hook with Vue/React.
When I do stream.switchThread('existing-thread-id'), stream.messages stays empty, no history loads.
How do I populate stream.messages with the thread’s existing history from the server?
Do I need to manually call client.threads.getState(threadId) and merge? Or is there a built-in way?
Minimal example:
const stream = useStream({ graphId: 'agent' });
await stream.switchThread('abc-123'); // stream.messages still empty!
Expected: stream.messages auto-fills with thread history.
Thanks!
Also fetchStateHistory from useStream doesn’t load history on mounting.
as for React, maye this is an issue:
// ❌ Wrong — graphId is not a valid option
const stream = useStream({ graphId: 'agent' });
// ✅ Correct
const stream = useStream({ assistantId: 'agent', apiUrl: '...' });
for Vue, I see there is a gap or bug, even two:
options.threadId is never read
- no
watch on threadId for history fetching
looking for workarounds…
potential workaround for Vue
Until this gap is addressed upstream, you can use the client instance returned by useStream to manually fetch and display thread history:
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { Client } from "@langchain/langgraph-sdk";
import { ref, computed, watch } from "vue";
const API_URL = "http://localhost:2024";
// Track which threadId to load (e.g., from URL params, a thread list, etc.)
const activeThreadId = ref<string | null>(getThreadIdFromUrl());
const stream = useStream({
assistantId: "agent",
apiUrl: API_URL,
});
// Manual history fetch — fills the gap that Vue's useStream doesn't cover
const manualMessages = ref<any[]>([]);
const isLoadingHistory = ref(false);
async function loadThreadHistory(threadId: string) {
isLoadingHistory.value = true;
try {
const state = await stream.client.threads.getState(threadId);
manualMessages.value = state.values?.messages ?? [];
} catch (e) {
console.error("Failed to load thread history:", e);
manualMessages.value = [];
} finally {
isLoadingHistory.value = false;
}
}
// When activeThreadId changes, switch thread AND load history
watch(activeThreadId, async (newId) => {
if (newId) {
stream.switchThread(newId); // So future submits target this thread
await loadThreadHistory(newId); // Manually fetch existing messages
} else {
stream.switchThread(null); // Reset for a new thread on next submit
manualMessages.value = [];
}
}, { immediate: true });
// Show stream messages when streaming, otherwise show manually loaded history.
// After a submit completes, stream.messages will contain the full conversation
// (since the server returns the complete state including history), so the
// manual messages naturally get "replaced."
const displayMessages = computed(() => {
const streamMsgs = stream.messages.value;
return streamMsgs.length > 0 ? streamMsgs : manualMessages.value;
});
function getThreadIdFromUrl(): string | null {
const params = new URLSearchParams(window.location.search);
return params.get("threadId");
}
</script>
<template>
<div>
<div v-if="isLoadingHistory">Loading thread history...</div>
<div v-for="(msg, i) in displayMessages" :key="msg.id ?? i">
<strong>{{ msg.type }}:</strong> {{ msg.content }}
</div>
<button
:disabled="stream.isLoading.value"
@click="stream.submit({ messages: [{ type: 'human', content: 'Hello!' }] })"
>
Send
</button>
</div>
</template>