diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index ffd1ef1..1ec3706 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -278,13 +278,19 @@ export function Chat() { // Three signals indicate "tools finished, now streaming the reply": // 1. `pendingFinal` — set by tool-result final events // 2. `allToolsCompleted` — all entries in streamingTools are completed - // 3. `hasCompletedToolPhase` — historical messages (loaded by the poll) - // contain tool_use blocks, meaning the Gateway executed tools - // server-side without sending streaming tool events to the client + // 3. `hasCompletedToolPhase` — the Gateway executed tools server-side + // (no streaming tool events) and the tool phase is over. Detected + // by finding tool_use in historical messages AND the last assistant + // message in the segment having no tool_use blocks (meaning the + // model has moved past tool calls into the reply phase). const allToolsCompleted = streamingTools.length > 0 && !hasRunningStreamToolStatus; - const hasCompletedToolPhase = segmentMessages.some((msg) => + const lastAssistantInSegment = [...segmentMessages].reverse().find((m) => m.role === 'assistant'); + const segmentHasTools = segmentMessages.some((msg) => msg.role === 'assistant' && extractToolUse(msg).length > 0, ); + const lastAssistantHasNoTools = lastAssistantInSegment != null + && extractToolUse(lastAssistantInSegment).length === 0; + const hasCompletedToolPhase = segmentHasTools && lastAssistantHasNoTools; const rawStreamingReplyCandidate = isLatestOpenRun && (pendingFinal || allToolsCompleted || hasCompletedToolPhase) && (hasStreamText || hasStreamImages) @@ -323,6 +329,16 @@ export function Chat() { } const cached = graphStepCache[runKey]; if (!cached) return []; + // When using cached steps for a completed run, filter out message + // steps whose text matches the final reply. The cache was captured + // during streaming when all text was narration; now that the run is + // complete the reply should not appear inside the graph. + const cachedReplyIdx = cached.replyIndex; + const replyMsg = cachedReplyIdx != null ? messages[cachedReplyIdx] : null; + const replyText = replyMsg ? extractText(replyMsg).trim() : ''; + const cleanedSteps = replyText + ? cached.steps.filter((s) => !(s.kind === 'message' && s.detail?.trim() === replyText)) + : cached.steps; return [{ triggerIndex: idx, replyIndex: cached.replyIndex, @@ -330,8 +346,8 @@ export function Chat() { agentLabel: cached.agentLabel, sessionLabel: cached.sessionLabel, segmentEnd: nextUserIndex === -1 ? messages.length - 1 : nextUserIndex - 1, - steps: cached.steps, - messageStepTexts: getPrimaryMessageStepTexts(cached.steps), + steps: cleanedSteps, + messageStepTexts: getPrimaryMessageStepTexts(cleanedSteps), streamingReplyText: null, }]; }