fix: Thinking indicator respringt nach chat:final durch trailing events
Nach chat:final kommen oft noch agent-Events rein (Core raeumt nach), die den Thinking-Indicator wieder anspringen liessen. - Diagnostic: 3s-Settled-Window nach chat:final, agent_activity-Broadcasts werden in dem Fenster unterdrueckt (idle kommt weiter durch). - Bridge: Gleiches Fenster in _emit_activity() — App bekommt keine trailing thinking/tool-Events mehr nach dem finalen Antwort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
271fc4edf6
commit
8c1014d281
|
|
@ -554,6 +554,9 @@ class ARIABridge:
|
||||||
|
|
||||||
# Letzter gesendeter agent_activity-State (zum Entduplizieren)
|
# Letzter gesendeter agent_activity-State (zum Entduplizieren)
|
||||||
self._last_activity_state: Optional[tuple] = None
|
self._last_activity_state: Optional[tuple] = None
|
||||||
|
# Zeitstempel des letzten chat:final — waehrend 3s danach werden
|
||||||
|
# trailing Agent-Events unterdrueckt (Core raeumt manchmal nach).
|
||||||
|
self._last_chat_final_at: float = 0.0
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
"""Initialisiert alle Komponenten.
|
"""Initialisiert alle Komponenten.
|
||||||
|
|
@ -779,6 +782,7 @@ class ARIABridge:
|
||||||
|
|
||||||
if state == "final":
|
if state == "final":
|
||||||
text = self._extract_chat_text(payload)
|
text = self._extract_chat_text(payload)
|
||||||
|
self._last_chat_final_at = asyncio.get_event_loop().time()
|
||||||
await self._emit_activity("idle", "")
|
await self._emit_activity("idle", "")
|
||||||
if not text:
|
if not text:
|
||||||
logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200])
|
logger.warning("[core] chat final ohne Text: %s", json.dumps(payload)[:200])
|
||||||
|
|
@ -790,6 +794,7 @@ class ARIABridge:
|
||||||
if state == "error":
|
if state == "error":
|
||||||
error = payload.get("error", "Unbekannt")
|
error = payload.get("error", "Unbekannt")
|
||||||
logger.error("[core] Chat-Fehler: %s", error)
|
logger.error("[core] Chat-Fehler: %s", error)
|
||||||
|
self._last_chat_final_at = asyncio.get_event_loop().time()
|
||||||
await self._emit_activity("idle", "")
|
await self._emit_activity("idle", "")
|
||||||
await self._send_to_rvs({
|
await self._send_to_rvs({
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
|
|
@ -1468,7 +1473,14 @@ class ARIABridge:
|
||||||
logger.info("[cancel] Diagnostic /api/cancel: %s", status)
|
logger.info("[cancel] Diagnostic /api/cancel: %s", status)
|
||||||
|
|
||||||
async def _emit_activity(self, activity: str, tool: str = "") -> None:
|
async def _emit_activity(self, activity: str, tool: str = "") -> None:
|
||||||
"""Sendet agent_activity an die App — nur wenn sich der State geaendert hat."""
|
"""Sendet agent_activity an die App — nur wenn sich der State geaendert hat.
|
||||||
|
|
||||||
|
Trailing Agent-Events nach chat:final werden 3s lang unterdrueckt
|
||||||
|
(nur 'idle' kommt immer durch)."""
|
||||||
|
if activity != "idle" and self._last_chat_final_at > 0:
|
||||||
|
since_final = asyncio.get_event_loop().time() - self._last_chat_final_at
|
||||||
|
if since_final < 3.0:
|
||||||
|
return
|
||||||
state = (activity, tool)
|
state = (activity, tool)
|
||||||
if state == self._last_activity_state:
|
if state == self._last_activity_state:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ const browserClients = new Set();
|
||||||
let pipelineActive = false;
|
let pipelineActive = false;
|
||||||
let pipelineStartTime = 0;
|
let pipelineStartTime = 0;
|
||||||
|
|
||||||
|
// Nach chat:final kommen oft noch Trailing Agent-Events. Waehrend dieses
|
||||||
|
// Fensters unterdruecken wir agent_activity-Broadcasts, damit der
|
||||||
|
// Thinking-Indicator nicht wieder anspringt.
|
||||||
|
let lastChatFinalAt = 0;
|
||||||
|
const SETTLED_WINDOW_MS = 3000;
|
||||||
|
|
||||||
function plog(message, level) {
|
function plog(message, level) {
|
||||||
const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : "";
|
const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : "";
|
||||||
const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` };
|
const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` };
|
||||||
|
|
@ -356,17 +362,22 @@ function handleGatewayMessage(msg) {
|
||||||
broadcast({ type: "chat_delta", delta, payload });
|
broadcast({ type: "chat_delta", delta, payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nach chat:final trickeln noch Aufraeum-Events rein — unterdruecken,
|
||||||
|
// damit der Thinking-Indicator nicht wieder anspringt.
|
||||||
|
const settled = lastChatFinalAt && (Date.now() - lastChatFinalAt) < SETTLED_WINDOW_MS;
|
||||||
|
|
||||||
// Tool-Nutzung erkennen und broadcasten
|
// Tool-Nutzung erkennen und broadcasten
|
||||||
if (stream === "tool_use" || data.type === "tool_use") {
|
if (stream === "tool_use" || data.type === "tool_use") {
|
||||||
const toolName = data.name || data.tool || payload.tool || "";
|
const toolName = data.name || data.tool || payload.tool || "";
|
||||||
if (toolName) {
|
if (toolName && !settled) {
|
||||||
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
|
broadcast({ type: "agent_activity", activity: "tool", tool: toolName, data });
|
||||||
log("info", "gateway", `Tool: ${toolName}`);
|
log("info", "gateway", `Tool: ${toolName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genereller Activity-Heartbeat (ARIA denkt)
|
if (!settled) {
|
||||||
broadcast({ type: "agent_activity", activity: stream || "thinking" });
|
broadcast({ type: "agent_activity", activity: stream || "thinking" });
|
||||||
|
}
|
||||||
updateAgentActivity();
|
updateAgentActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -381,6 +392,7 @@ function handleGatewayMessage(msg) {
|
||||||
if (runId && seenFinalRuns.has(runId)) return; // Duplikat
|
if (runId && seenFinalRuns.has(runId)) return; // Duplikat
|
||||||
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
|
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
|
||||||
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
||||||
|
lastChatFinalAt = Date.now();
|
||||||
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
||||||
broadcast({ type: "chat_final", text, payload });
|
broadcast({ type: "chat_final", text, payload });
|
||||||
broadcast({ type: "agent_activity", activity: "idle" });
|
broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
|
|
@ -424,6 +436,7 @@ function handleGatewayMessage(msg) {
|
||||||
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
|
if (runId) { seenFinalRuns.add(runId); setTimeout(() => seenFinalRuns.delete(runId), 60000); }
|
||||||
const text = extractChatText(payload) || payload.text || "";
|
const text = extractChatText(payload) || payload.text || "";
|
||||||
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
||||||
|
lastChatFinalAt = Date.now();
|
||||||
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`);
|
||||||
else broadcast({ type: "agent_activity", activity: "idle" });
|
else broadcast({ type: "agent_activity", activity: "idle" });
|
||||||
broadcast({ type: "chat_final", text, payload });
|
broadcast({ type: "chat_final", text, payload });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue