mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
perf: replace agents polling with WebSocket, fix workbox prod mode (#254)
This commit is contained in:
@@ -103,3 +103,6 @@ MagicMock/
|
||||
|
||||
# Second Repos
|
||||
Inline/
|
||||
|
||||
# Local git worktrees
|
||||
.worktrees/
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { getBase } from './api';
|
||||
|
||||
export interface AgentEvent {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function buildWsUrl(agentId?: string): string {
|
||||
const base = getBase();
|
||||
let origin: string;
|
||||
if (base) {
|
||||
origin = base.replace(/^http/, 'ws');
|
||||
} else {
|
||||
const loc = window.location;
|
||||
origin = `${loc.protocol === 'https:' ? 'wss:' : 'ws:'}//${loc.host}`;
|
||||
}
|
||||
const path = '/v1/agents/events';
|
||||
return agentId
|
||||
? `${origin}${path}?agent_id=${encodeURIComponent(agentId)}`
|
||||
: `${origin}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to agent events over WebSocket.
|
||||
* Auto-reconnects with backoff when the socket drops.
|
||||
*/
|
||||
export function useAgentEvents(
|
||||
agentId: string | undefined,
|
||||
onEvent: (event: AgentEvent) => void,
|
||||
eventTypes?: readonly string[],
|
||||
): void {
|
||||
const onEventRef = useRef(onEvent);
|
||||
onEventRef.current = onEvent;
|
||||
const typesRef = useRef(eventTypes);
|
||||
typesRef.current = eventTypes;
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentId) return;
|
||||
let ws: WebSocket | null = null;
|
||||
let closed = false;
|
||||
let retry = 0;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const connect = () => {
|
||||
if (closed) return;
|
||||
try {
|
||||
ws = new WebSocket(buildWsUrl(agentId));
|
||||
} catch {
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
ws.onopen = () => {
|
||||
retry = 0;
|
||||
};
|
||||
ws.onmessage = (msg) => {
|
||||
try {
|
||||
const payload = JSON.parse(msg.data) as AgentEvent;
|
||||
const allowed = typesRef.current;
|
||||
if (allowed && !allowed.includes(payload.type)) return;
|
||||
onEventRef.current(payload);
|
||||
} catch {
|
||||
// ignore malformed payload
|
||||
}
|
||||
};
|
||||
ws.onclose = () => {
|
||||
if (!closed) schedule();
|
||||
};
|
||||
ws.onerror = () => {
|
||||
ws?.close();
|
||||
};
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (closed) return;
|
||||
const delay = Math.min(30000, 1000 * 2 ** Math.min(retry, 5));
|
||||
retry += 1;
|
||||
reconnectTimer = setTimeout(connect, delay);
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
closed = true;
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
ws?.close();
|
||||
};
|
||||
}, [agentId]);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
sendblueHealth,
|
||||
} from '../lib/api';
|
||||
import type { AgentTask, ChannelBinding, AgentTemplate, AgentMessage, ManagedAgent, LearningLogEntry, AgentTrace, ToolInfo } from '../lib/api';
|
||||
import { useAgentEvents } from '../lib/useAgentEvents';
|
||||
import {
|
||||
Plus,
|
||||
Bot,
|
||||
@@ -1332,10 +1333,21 @@ function InteractTab({ agentId, agentStatus }: { agentId: string; agentStatus: s
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
const interval = setInterval(loadData, 2000);
|
||||
// Fallback slow poll — WS is primary, this catches missed events / dropped sockets
|
||||
const interval = setInterval(loadData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [loadData]);
|
||||
|
||||
// Event-driven refresh — fires when the server reports agent activity
|
||||
useAgentEvents(agentId, loadData, [
|
||||
'agent_tick_start',
|
||||
'agent_tick_end',
|
||||
'agent_tick_error',
|
||||
'agent_message_received',
|
||||
'tool_call_end',
|
||||
'inference_end',
|
||||
]);
|
||||
|
||||
useEffect(() => { setLiveStatus(agentStatus); }, [agentStatus]);
|
||||
|
||||
// Clean up elapsed-time timer on unmount
|
||||
@@ -2854,10 +2866,20 @@ function LogsTab({ agentId }: { agentId: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
const interval = setInterval(loadData, 5000);
|
||||
// Fallback slow poll — WS is primary, this catches missed events
|
||||
const interval = setInterval(loadData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [loadData]);
|
||||
|
||||
// Event-driven refresh — trace/learning entries are created by tick + tool events
|
||||
useAgentEvents(agentId, loadData, [
|
||||
'agent_tick_end',
|
||||
'agent_tick_error',
|
||||
'tool_call_end',
|
||||
'inference_end',
|
||||
'agent_learning_completed',
|
||||
]);
|
||||
|
||||
// Merge traces and learning entries into a unified timeline
|
||||
type TimelineEntry =
|
||||
| { kind: 'trace'; data: AgentTrace; ts: number }
|
||||
|
||||
@@ -30,7 +30,6 @@ export default defineConfig({
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||
navigateFallbackDenylist: [/^\/v1\//, /^\/health/, /^\/dashboard/],
|
||||
mode: 'development',
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -141,7 +141,7 @@ impl ICLUpdaterPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
skills.sort_by(|a, b| b.occurrences.cmp(&a.occurrences));
|
||||
skills.sort_by_key(|s| std::cmp::Reverse(s.occurrences));
|
||||
self.discovered_skills = skills;
|
||||
&self.discovered_skills
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ impl TrainingDataMiner {
|
||||
}
|
||||
}
|
||||
let mut ranked: Vec<_> = tool_freq.into_iter().collect();
|
||||
ranked.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
ranked.sort_by_key(|r| std::cmp::Reverse(r.1));
|
||||
let best_tools: Vec<String> = ranked.into_iter().map(|(name, _)| name).collect();
|
||||
|
||||
let all_scores: Vec<f64> = agent_scores.values().flat_map(|v| v.iter().copied()).collect();
|
||||
|
||||
@@ -136,13 +136,10 @@ impl WorkflowGraph {
|
||||
continue;
|
||||
}
|
||||
if let Some(&vi) = self.node_index.get(&edge.target) {
|
||||
match color[vi] {
|
||||
let c = color[vi];
|
||||
match c {
|
||||
1 => return true,
|
||||
0 => {
|
||||
if self.dfs_has_cycle(vi, color) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
0 if self.dfs_has_cycle(vi, color) => return true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user