import type { Message } from "@langchain/langgraph-sdk"; import { describe, expect, test } from "vitest"; import { extractContentFromMessage, extractReasoningContentFromMessage, getAssistantTurnCopyData, getAssistantTurnUsageMessages, getMessageGroups, getStreamingMessageLookup, hasContent, hasReasoning, isAssistantMessageGroupStreaming, } from "@/core/messages/utils"; function aiMessage(content: string): Message { return { id: "ai-1", type: "ai", content, } as Message; } test("aggregates token usage messages once per assistant turn", () => { const messages = [ { id: "human-1", type: "human", content: "Plan a trip", }, { id: "ai-1", type: "ai", content: "", tool_calls: [{ id: "tool-1", name: "web_search", args: {} }], usage_metadata: { input_tokens: 10, output_tokens: 5, total_tokens: 15 }, }, { id: "tool-1-result", type: "tool", name: "web_search", tool_call_id: "tool-1", content: "[]", }, { id: "ai-2", type: "ai", content: "Here is the itinerary", usage_metadata: { input_tokens: 2, output_tokens: 8, total_tokens: 10 }, }, { id: "human-2", type: "human", content: "Make it shorter", }, { id: "ai-3", type: "ai", content: "Short version", usage_metadata: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, }, ] as Message[]; const groups = getMessageGroups(messages); const usageMessagesByGroupIndex = getAssistantTurnUsageMessages(groups); expect(groups.map((group) => group.type)).toEqual([ "human", "assistant:processing", "assistant", "human", "assistant", ]); expect( usageMessagesByGroupIndex.map( (groupMessages) => groupMessages?.map((message) => message.id) ?? null, ), ).toEqual([null, null, ["ai-1", "ai-2"], null, ["ai-3"]]); }); describe("inline tag splitting", () => { test("strips a fully closed block from AI content", () => { const message = aiMessage("internal reasoningfinal answer"); expect(extractContentFromMessage(message)).toBe("final answer"); expect(extractReasoningContentFromMessage(message)).toBe( "internal reasoning", ); }); test("strips multiple closed blocks and joins their reasoning", () => { const message = aiMessage( "step onebetweenstep twoafter", ); expect(extractContentFromMessage(message)).toBe("betweenafter"); expect(extractReasoningContentFromMessage(message)).toBe( "step one\n\nstep two", ); }); test("during streaming, an unclosed tag does not leak its tail into content", () => { // Simulates accumulated content mid-stream, before arrives. const message = aiMessage( "I need to analyze the user's question step by", ); expect(extractContentFromMessage(message)).toBe(""); expect(extractContentFromMessage(message)).not.toContain(""); expect(extractReasoningContentFromMessage(message)).toBe( "I need to analyze the user's question step by", ); }); test("preamble before an unclosed stays in content", () => { const message = aiMessage( "Here is part of the answer.but wait, let me reconsider", ); expect(extractContentFromMessage(message)).toBe( "Here is part of the answer.", ); expect(extractReasoningContentFromMessage(message)).toBe( "but wait, let me reconsider", ); }); test("closed followed by a trailing unclosed merges both into reasoning", () => { const message = aiMessage( "first steppartial answersecond step still streaming", ); expect(extractContentFromMessage(message)).toBe("partial answer"); expect(extractReasoningContentFromMessage(message)).toBe( "first step\n\nsecond step still streaming", ); }); test("hasReasoning recognises an unclosed tag mid-stream", () => { expect(hasReasoning(aiMessage("thinking in progress"))).toBe(true); }); test("hasContent excludes an unclosed tail when no preamble exists", () => { expect(hasContent(aiMessage("thinking in progress"))).toBe(false); }); test("hasContent stays true when preamble precedes an unclosed ", () => { expect(hasContent(aiMessage("preamblestill thinking"))).toBe(true); }); test("a lone open tag with no body yields no reasoning and no content", () => { const message = aiMessage(""); expect(extractContentFromMessage(message)).toBe(""); expect(extractReasoningContentFromMessage(message)).toBeNull(); expect(hasReasoning(message)).toBe(false); }); test("a literal inside markdown inline code is not treated as reasoning", () => { const message = aiMessage( "Use `` markers to delimit reasoning sections.", ); expect(extractContentFromMessage(message)).toBe( "Use `` markers to delimit reasoning sections.", ); expect(extractReasoningContentFromMessage(message)).toBeNull(); expect(hasReasoning(message)).toBe(false); }); test("a backtick-prefixed mid-stream is not split into reasoning", () => { // Simulates the moment the model has emitted the opening backtick and // `` for a literal documentation reference, before the closing // backtick arrives. The pre-fix behaviour would have permanently // truncated the content here. const message = aiMessage("Documentation: `"); expect(extractContentFromMessage(message)).toBe("Documentation: `"); expect(extractReasoningContentFromMessage(message)).toBeNull(); }); }); test("hides internal todo reminder messages from message groups", () => { const messages = [ { id: "human-1", type: "human", content: "Audit the middleware", }, { id: "todo-reminder-1", type: "human", name: "todo_completion_reminder", content: "finish todos", }, { id: "todo-reminder-2", type: "human", name: "todo_reminder", content: "remember todos", }, { id: "ai-1", type: "ai", content: "Done", }, ] as Message[]; const groups = getMessageGroups(messages); expect(groups.map((group) => group.type)).toEqual(["human", "assistant"]); expect( groups.flatMap((group) => group.messages).map((message) => message.id), ).toEqual(["human-1", "ai-1"]); }); test("hides assistant copy data while that turn is streaming", () => { const messages = [ { id: "ai-1", type: "ai", content: "Partial answer", }, ] as Message[]; expect(getAssistantTurnCopyData(messages)).toBe("Partial answer"); expect(getAssistantTurnCopyData(messages, { isStreaming: true })).toBeNull(); }); test("marks the latest assistant message as streaming", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Still generating", }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true, () => ({ streamMetadata: { langgraph_node: "agent" }, })), ), ).toBe(true); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, false, () => ({ streamMetadata: { langgraph_node: "agent" }, })), ), ).toBe(false); }); test("keeps previous assistant copyable while waiting for a new visible answer", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Completed answer", }, { id: "opt-human-1", type: "human", content: "Continue", }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true), ), ).toBe(false); }); test("keeps previous assistant copyable while a hidden send is starting", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Completed answer", }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true), ), ).toBe(false); }); test("keeps previous assistant copyable after a hidden send is appended", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Completed answer", }, { id: "human-hidden", type: "human", content: "Save this agent", additional_kwargs: { hide_from_ui: true }, }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true), ), ).toBe(false); }); test("uses stream metadata to identify an assistant before optimistic input", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Completed answer", }, { id: "ai-2", type: "ai", content: "Still generating", }, { id: "opt-human-1", type: "human", content: "Continue", }, ] as Message[]; const assistantGroups = getMessageGroups(messages).filter( (group) => group.type === "assistant", ); const groups = getMessageGroups(messages); const assistantGroupIndexes = groups .map((group, index) => (group.type === "assistant" ? index : -1)) .filter((index) => index >= 0); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndexes[0] ?? -1]?.messages ?? [], getStreamingMessageLookup(messages, true, (message) => message.id === "ai-2" ? { streamMetadata: { langgraph_node: "agent" } } : undefined, ), ), ).toBe(false); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndexes[1] ?? -1]?.messages ?? [], getStreamingMessageLookup(messages, true, (message) => message.id === "ai-2" ? { streamMetadata: { langgraph_node: "agent" } } : undefined, ), ), ).toBe(true); expect(assistantGroups.map((group) => group.id)).toEqual(["ai-1", "ai-2"]); }); test("does not mark a completed assistant group streaming from a later processing group", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Visible answer", }, { id: "ai-2", type: "ai", content: "", tool_calls: [{ id: "tool-1", name: "web_search", args: {} }], }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect(groups.map((group) => group.type)).toEqual([ "human", "assistant", "assistant:processing", ]); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true, (message) => message.id === "ai-2" ? { streamMetadata: { langgraph_node: "agent" } } : undefined, ), ), ).toBe(false); }); test("keeps streaming assistant hidden when a hidden control message follows it", () => { const messages = [ { id: "human-1", type: "human", content: "Hello", }, { id: "ai-1", type: "ai", content: "Still generating", }, { id: "human-hidden", type: "human", content: "Save this agent", additional_kwargs: { hide_from_ui: true }, }, ] as Message[]; const groups = getMessageGroups(messages); const assistantGroupIndex = groups.findIndex( (group) => group.type === "assistant", ); expect( isAssistantMessageGroupStreaming( groups[assistantGroupIndex]?.messages ?? [], getStreamingMessageLookup(messages, true, (message) => message.id === "ai-1" ? { streamMetadata: { langgraph_node: "agent" } } : undefined, ), ), ).toBe(true); });