feat(ui): Enhanced agent thinking detection for Ollama models
- Added semantic detection module (agent-status-detection.ts) that analyzes message content for keywords like 'standby', 'processing', 'analyzing' - Updated isAgentThinking() in multi-task-chat to use semantic detection when streaming has technically ended but agent is conceptually working - Added dynamic status messages (PROCESSING, AGENT PROCESSING, etc.) instead of static THINKING/STREAMING labels - Enhanced session-status.ts to check semantic content before returning idle - Fixes issue where Ollama models output status messages and pause, causing UI to incorrectly show ready-to-send state
This commit is contained in:
@@ -10,6 +10,7 @@ import { addToTaskQueue, getSoloState, setActiveTaskId, toggleAutonomous, toggle
|
||||
import { getLogger } from "@/lib/logger";
|
||||
import { clearCompactionSuggestion, getCompactionSuggestion } from "@/stores/session-compaction";
|
||||
import { emitSessionSidebarRequest } from "@/lib/session-sidebar-events";
|
||||
import { detectAgentWorkingState, getAgentStatusMessage } from "@/lib/agent-status-detection";
|
||||
import {
|
||||
Command,
|
||||
Plus,
|
||||
@@ -216,7 +217,36 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
|
||||
const store = messageStore();
|
||||
const lastMsg = store.getMessage(ids[ids.length - 1]);
|
||||
return lastMsg?.role === "assistant" && (lastMsg.status === "streaming" || lastMsg.status === "sending");
|
||||
|
||||
// Basic check: streaming or sending status
|
||||
if (lastMsg?.role === "assistant" && (lastMsg.status === "streaming" || lastMsg.status === "sending")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enhanced check: semantic detection for "standby", "processing" messages
|
||||
// This catches Ollama models that output status messages and pause
|
||||
if (lastMsg?.role === "assistant") {
|
||||
const workingState = detectAgentWorkingState(lastMsg);
|
||||
return workingState.isWorking;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Get dynamic status message for display
|
||||
const agentStatusMessage = createMemo(() => {
|
||||
const ids = filteredMessageIds();
|
||||
if (ids.length === 0) return "THINKING";
|
||||
|
||||
const store = messageStore();
|
||||
const lastMsg = store.getMessage(ids[ids.length - 1]);
|
||||
|
||||
if (!lastMsg || lastMsg.role !== "assistant") {
|
||||
return "THINKING";
|
||||
}
|
||||
|
||||
const statusMsg = getAgentStatusMessage(lastMsg);
|
||||
return statusMsg?.toUpperCase() || "THINKING";
|
||||
});
|
||||
|
||||
// Auto-scroll during streaming - DISABLED for performance testing
|
||||
@@ -539,7 +569,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
<Show when={isAgentThinking()}>
|
||||
<div class="flex items-center space-x-2 px-3 py-1.5 bg-violet-500/15 border border-violet-500/30 rounded-lg animate-pulse shadow-[0_0_20px_rgba(139,92,246,0.2)]">
|
||||
<Sparkles size={12} class="text-violet-400 animate-spin" style={{ "animation-duration": "3s" }} />
|
||||
<span class="text-[10px] font-black text-violet-400 uppercase tracking-tight">Streaming</span>
|
||||
<span class="text-[10px] font-black text-violet-400 uppercase tracking-tight">{agentStatusMessage()}</span>
|
||||
<span class="text-[10px] font-bold text-violet-300">{formatTokenTotal(tokenStats().used)}</span>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -846,7 +876,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "150ms" }} />
|
||||
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "300ms" }} />
|
||||
</div>
|
||||
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "THINKING" : "SENDING"}</span>
|
||||
<span class="text-[9px] font-bold text-indigo-400">{isSending() ? "SENDING" : agentStatusMessage()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
249
packages/ui/src/lib/agent-status-detection.ts
Normal file
249
packages/ui/src/lib/agent-status-detection.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Agent Status Detection Module
|
||||
*
|
||||
* Provides intelligent detection of when an agent is still "working" even after
|
||||
* streaming has technically completed. This handles cases where:
|
||||
* 1. Agent outputs "standby", "processing", "working" messages
|
||||
* 2. Agent is in multi-step reasoning mode
|
||||
* 3. Ollama models pause between thinking and output phases
|
||||
*/
|
||||
|
||||
import type { MessageRecord } from "../stores/message-v2/types"
|
||||
|
||||
// Keywords that indicate the agent is still processing
|
||||
const WORKING_KEYWORDS = [
|
||||
"standby",
|
||||
"stand by",
|
||||
"processing",
|
||||
"please wait",
|
||||
"working on",
|
||||
"analyzing",
|
||||
"thinking",
|
||||
"computing",
|
||||
"calculating",
|
||||
"evaluating",
|
||||
"generating",
|
||||
"preparing",
|
||||
"loading",
|
||||
"fetching",
|
||||
"retrieving",
|
||||
"in progress",
|
||||
"one moment",
|
||||
"hold on",
|
||||
"just a sec",
|
||||
"give me a moment",
|
||||
"let me",
|
||||
"i'll",
|
||||
"i will",
|
||||
"checking",
|
||||
"scanning",
|
||||
"searching",
|
||||
"looking",
|
||||
"finding"
|
||||
] as const
|
||||
|
||||
// Keywords that indicate the agent has finished
|
||||
const COMPLETION_KEYWORDS = [
|
||||
"here is",
|
||||
"here's",
|
||||
"here are",
|
||||
"done",
|
||||
"complete",
|
||||
"finished",
|
||||
"result",
|
||||
"solution",
|
||||
"answer",
|
||||
"output",
|
||||
"summary",
|
||||
"conclusion",
|
||||
"final",
|
||||
"successfully",
|
||||
"implemented",
|
||||
"fixed",
|
||||
"resolved",
|
||||
"created",
|
||||
"updated"
|
||||
] as const
|
||||
|
||||
// Patterns that strongly indicate agent is still working
|
||||
const WORKING_PATTERNS = [
|
||||
/stand\s*by/i,
|
||||
/processing\s*(complete\s*)?data/i,
|
||||
/please\s+wait/i,
|
||||
/working\s+on/i,
|
||||
/analyzing/i,
|
||||
/\bwait\b/i,
|
||||
/\bone\s+moment\b/i,
|
||||
/\bhold\s+on\b/i,
|
||||
/\.\.\.\s*$/, // Ends with ellipsis
|
||||
/…\s*$/, // Ends with unicode ellipsis
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Extracts text content from a message's parts
|
||||
*/
|
||||
function extractMessageText(message: MessageRecord): string {
|
||||
const textParts: string[] = []
|
||||
|
||||
for (const partId of message.partIds) {
|
||||
const part = message.parts[partId]
|
||||
if (part?.data) {
|
||||
const data = part.data as Record<string, unknown>
|
||||
if (data.type === "text" && typeof data.text === "string") {
|
||||
textParts.push(data.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textParts.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last N characters of a message for keyword detection
|
||||
*/
|
||||
function getRecentContent(message: MessageRecord, charLimit = 500): string {
|
||||
const fullText = extractMessageText(message)
|
||||
if (fullText.length <= charLimit) {
|
||||
return fullText.toLowerCase()
|
||||
}
|
||||
return fullText.slice(-charLimit).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the message content indicates the agent is still working
|
||||
*/
|
||||
export function detectAgentWorkingState(message: MessageRecord | null | undefined): {
|
||||
isWorking: boolean
|
||||
reason?: string
|
||||
confidence: "high" | "medium" | "low"
|
||||
} {
|
||||
if (!message) {
|
||||
return { isWorking: false, confidence: "high" }
|
||||
}
|
||||
|
||||
// If message status is streaming or sending, definitely working
|
||||
if (message.status === "streaming" || message.status === "sending") {
|
||||
return { isWorking: true, reason: "Active streaming", confidence: "high" }
|
||||
}
|
||||
|
||||
// Get recent content to analyze
|
||||
const recentContent = getRecentContent(message)
|
||||
|
||||
if (!recentContent) {
|
||||
return { isWorking: false, confidence: "high" }
|
||||
}
|
||||
|
||||
// Check for working patterns with high confidence
|
||||
for (const pattern of WORKING_PATTERNS) {
|
||||
if (pattern.test(recentContent)) {
|
||||
return {
|
||||
isWorking: true,
|
||||
reason: `Pattern match: ${pattern.source}`,
|
||||
confidence: "high"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if recent content ends with working keywords
|
||||
const lastLine = recentContent.split("\n").pop()?.trim() || ""
|
||||
|
||||
for (const keyword of WORKING_KEYWORDS) {
|
||||
if (lastLine.includes(keyword)) {
|
||||
// Check if there's also a completion keyword nearby
|
||||
const hasCompletionNearby = COMPLETION_KEYWORDS.some(ck =>
|
||||
recentContent.slice(-200).includes(ck)
|
||||
)
|
||||
|
||||
if (!hasCompletionNearby) {
|
||||
return {
|
||||
isWorking: true,
|
||||
reason: `Working keyword: "${keyword}"`,
|
||||
confidence: "medium"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check message age - if very recent and short, might still be working
|
||||
const now = Date.now()
|
||||
const messageAge = now - message.updatedAt
|
||||
const contentLength = extractMessageText(message).length
|
||||
|
||||
// If message was updated very recently (< 2s) and content is short
|
||||
if (messageAge < 2000 && contentLength < 100) {
|
||||
return {
|
||||
isWorking: true,
|
||||
reason: "Recently updated with short content",
|
||||
confidence: "low"
|
||||
}
|
||||
}
|
||||
|
||||
return { isWorking: false, confidence: "high" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last assistant message indicates agent is still conceptually working
|
||||
*/
|
||||
export function isAgentConceptuallyThinking(
|
||||
messages: MessageRecord[],
|
||||
lastAssistantMessage: MessageRecord | null | undefined
|
||||
): boolean {
|
||||
if (!lastAssistantMessage) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if message status indicates active work
|
||||
if (lastAssistantMessage.status === "streaming" ||
|
||||
lastAssistantMessage.status === "sending") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Use semantic detection
|
||||
const workingState = detectAgentWorkingState(lastAssistantMessage)
|
||||
return workingState.isWorking
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user-friendly status message for the current agent state
|
||||
*/
|
||||
export function getAgentStatusMessage(
|
||||
message: MessageRecord | null | undefined
|
||||
): string | null {
|
||||
if (!message) {
|
||||
return null
|
||||
}
|
||||
|
||||
const workingState = detectAgentWorkingState(message)
|
||||
|
||||
if (!workingState.isWorking) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (message.status === "streaming") {
|
||||
return "Streaming..."
|
||||
}
|
||||
|
||||
if (message.status === "sending") {
|
||||
return "Sending..."
|
||||
}
|
||||
|
||||
// Based on reason
|
||||
if (workingState.reason?.includes("standby") ||
|
||||
workingState.reason?.includes("stand by")) {
|
||||
return "Agent processing..."
|
||||
}
|
||||
|
||||
if (workingState.reason?.includes("processing")) {
|
||||
return "Processing..."
|
||||
}
|
||||
|
||||
if (workingState.reason?.includes("analyzing")) {
|
||||
return "Analyzing..."
|
||||
}
|
||||
|
||||
if (workingState.reason?.includes("ellipsis")) {
|
||||
return "Thinking..."
|
||||
}
|
||||
|
||||
return "Working..."
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { MessageRecord } from "./message-v2/types"
|
||||
import { sessions } from "./sessions"
|
||||
import { getSessionCompactionState } from "./session-compaction"
|
||||
import { messageStoreBus } from "./message-v2/bus"
|
||||
import { detectAgentWorkingState } from "../lib/agent-status-detection"
|
||||
|
||||
function getSession(instanceId: string, sessionId: string): Session | null {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
@@ -159,6 +160,15 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
|
||||
return "working"
|
||||
}
|
||||
|
||||
// Enhanced: Check if last assistant message content suggests agent is still working
|
||||
// This catches Ollama models that output "standby", "processing" messages and pause
|
||||
if (lastRecord && lastRecord.role === "assistant") {
|
||||
const workingState = detectAgentWorkingState(lastRecord)
|
||||
if (workingState.isWorking && workingState.confidence !== "low") {
|
||||
return "working"
|
||||
}
|
||||
}
|
||||
|
||||
return "idle"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user