swarm-os / frontend /src /hooks /useSimulation.js
aryxn323's picture
fix: custom prompts run single task only, not all 3 default tasks
deafe3d
import { useCallback, useRef, useEffect } from 'react';
import { useSimulationState, useSimulationDispatch } from '../store/simulationStore';
import { PRIMARY_SCENARIO, AGENT_ROLES, M2M_TRANSLATIONS } from '../data/scenarios';
const LOCAL_HTTP_BASE = 'http://localhost:8000';
const LOCAL_WS_BASE = 'ws://localhost:8000/ws';
export function getApiBase() {
const configured = import.meta.env.VITE_API_BASE_URL?.trim();
if (configured) return configured.replace(/\/$/, '');
if (typeof window === 'undefined') return LOCAL_HTTP_BASE;
const host = window.location.hostname;
const localHost = host === 'localhost' || host === '127.0.0.1';
return localHost ? LOCAL_HTTP_BASE : window.location.origin;
}
function getWebSocketUrl() {
const configured = import.meta.env.VITE_WS_URL?.trim();
if (configured) return configured;
if (typeof window === 'undefined') return LOCAL_WS_BASE;
const host = window.location.hostname;
const localHost = host === 'localhost' || host === '127.0.0.1';
if (localHost) return LOCAL_WS_BASE;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}/ws`;
}
function normalizePromptText(prompt) {
let text = (prompt || '').replace(/\r\n/g, '\n').trim();
if (!text) return '';
const noiseMarkers = [
'\nAuto-Generated RCA',
'\n202',
'\nINFO:',
'\nCommander\n',
'\nManager\n',
'\nDetective\n',
'\nCoder\n',
'\nFixes Applied',
'\nEscalations',
'\nResolution',
];
let cutIdx = text.length;
noiseMarkers.forEach((marker) => {
const idx = text.indexOf(marker);
if (idx !== -1) cutIdx = Math.min(cutIdx, idx);
});
text = text.slice(0, cutIdx).trim();
for (let i = 0; i < 3; i += 1) {
const wrapped = text.match(/^\[ORCHESTRATION_REQUEST\]\s*Parse intent:\s*"([\s\S]*)"\s*$/i);
if (wrapped) {
text = wrapped[1].trim();
continue;
}
text = text.replace(/^\[ORCHESTRATION_REQUEST\]\s*Parse intent:\s*/i, '').trim();
break;
}
return text.replace(/^["']+|["']+$/g, '').trim();
}
export function useSimulation() {
const state = useSimulationState();
if (typeof window !== 'undefined') {
window.__mockModeRef = state.mockMode;
}
const dispatch = useSimulationDispatch();
const timersRef = useRef([]);
const tickRef = useRef(null);
const stateRef = useRef(state);
const apiBase = getApiBase();
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
if (!state.isRunning || state.scenarioComplete) {
if (tickRef.current) {
clearInterval(tickRef.current);
tickRef.current = null;
}
}
}, [state.isRunning, state.scenarioComplete]);
const clearAllTimers = useCallback(() => {
timersRef.current.forEach(t => clearTimeout(t));
timersRef.current = [];
if (tickRef.current) {
clearInterval(tickRef.current);
tickRef.current = null;
}
}, []);
const orchestrate = useCallback((prompt, { customOnly = false } = {}) => {
clearAllTimers();
const cleanPrompt = normalizePromptText(prompt);
dispatch({ type: 'START_SIMULATION' });
// Add initial orchestration chat message
dispatch({
type: 'ADD_MESSAGE',
payload: {
id: Date.now().toString(),
agent: 'MANAGER',
m2m: `[ORCHESTRATION_REQUEST] Parse intent: "${cleanPrompt}"`,
think: customOnly
? 'Running custom prompt against a single OpenEnv task.'
: 'Sending prompt to the orchestrator so it can select the OpenEnv task, validator mode, and live evidence path.',
timestamp: new Date().toISOString()
}
});
fetch(`${apiBase}/api/orchestrate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: cleanPrompt, custom_only: customOnly })
})
.then(res => res.json())
.then(data => {
if (data.status === 'error') {
dispatch({
type: 'ADD_MESSAGE',
payload: {
id: Date.now().toString(),
agent: 'COMMANDER',
m2m: '[ORCHESTRATION_ERROR] Provider unavailable',
think: data.error || 'The backend could not start the live OpenEnv run.',
timestamp: new Date().toISOString()
}
});
return;
}
// Backend LLM parsed the constraints and launched the sandbox
dispatch({
type: 'ADD_MESSAGE',
payload: {
id: Date.now().toString(),
agent: 'COMMANDER',
m2m: `[JSON_PROVISION] ${JSON.stringify(data.commander_payload)}`,
think: `OpenEnv orchestration armed for ${data.commander_payload?.title || 'incident response'}. Validator: ${data.commander_payload?.validator_runtime?.label || 'unknown'}.`,
timestamp: new Date().toISOString()
}
});
})
.catch(err => console.warn("Backend unavailable:", err));
// Start the tick clock
const tickInterval = 1000;
tickRef.current = setInterval(() => {
dispatch({ type: 'TICK', payload: tickInterval });
}, tickInterval);
// Remove fake hardcoded PRIMARY_SCENARIO events. We rely entirely on WebSockets and API returns now!
}, [apiBase, dispatch, clearAllTimers]);
const start = useCallback(() => {
orchestrate("Deploy Llama-3 with a strict 500MB constraint");
}, [orchestrate]);
const pause = useCallback(() => {
dispatch({ type: 'PAUSE_SIMULATION' });
if (tickRef.current) {
clearInterval(tickRef.current);
tickRef.current = null;
}
}, [dispatch]);
const resume = useCallback(() => {
dispatch({ type: 'RESUME_SIMULATION' });
tickRef.current = setInterval(() => {
dispatch({ type: 'TICK', payload: 1000 });
}, 1000);
}, [dispatch]);
const stop = useCallback(() => {
clearAllTimers();
dispatch({ type: 'STOP_SIMULATION' });
}, [dispatch, clearAllTimers]);
const setSpeed = useCallback((speed) => {
dispatch({ type: 'SET_SPEED', payload: speed });
}, [dispatch]);
const toggleMockMode = useCallback(() => {
dispatch({ type: 'TOGGLE_MOCK_MODE' });
}, [dispatch]);
// WebSocket logic has been moved to SimulationProvider for global stability.
// useSimulation now focuses only on control functions and local timers.
return { start, pause, resume, stop, setSpeed, toggleMockMode, orchestrate };
}
function processEvent(event, dispatch) {
switch (event.type) {
case 'chat': {
const agentInfo = AGENT_ROLES[event.payload.agent] || { id: event.payload.agent.toLowerCase(), name: event.payload.agent, emoji: '🤖', color: '#71717a' };
// API Integration: Code Submission
if (event.payload.m2m.startsWith('CODE_SUBMIT')) {
const isMock = typeof window !== 'undefined' && window.__mockModeRef !== undefined ? window.__mockModeRef : true;
fetch(`${getApiBase()}/api/code/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: "# Live inference.py submission",
filename: event.payload.m2m.split(' | ')[1].trim(),
agent_role: event.payload.agent,
mock_mode: isMock
})
}).catch(err => console.warn("Backend unavailable:", err));
}
dispatch({
type: 'ADD_MESSAGE',
payload: {
agent: agentInfo,
m2m: event.payload.m2m,
english: M2M_TRANSLATIONS[event.payload.m2m] || event.payload.m2m,
think: event.payload.think || null,
},
});
break;
}
case 'telemetry':
dispatch({ type: 'UPDATE_TELEMETRY', payload: event.payload });
break;
case 'causal':
dispatch({ type: 'ADD_CAUSAL_NODE', payload: event.payload });
break;
case 'preflight':
dispatch({ type: 'UPDATE_PREFLIGHT', payload: event.payload });
break;
case 'disagreement':
dispatch({ type: 'SET_DISAGREEMENT', payload: event.payload });
break;
case 'git':
// API Integration: RCA Generation
if (event.payload.files && event.payload.files.includes('RCA_REPORT.md')) {
fetch(`${getApiBase()}/api/rca`)
.then(res => res.json())
.then(data => dispatch({ type: 'SET_RCA_DOCUMENT', payload: data.rca }))
.catch(() => console.warn("Using static RCA"));
}
dispatch({ type: 'ADD_GIT_COMMIT', payload: event.payload });
break;
case 'counterfactual':
// API Integration: Counterfactual
fetch(`${getApiBase()}/api/counterfactual`)
.then(res => res.json())
.then(data => {
dispatch({ type: 'SET_COUNTERFACTUAL', payload: {
actual: event.payload.actual,
dead: {
time: data.time_elapsed_formatted || data.time_elapsed_seconds + 's',
cost: `$${data.projected_cost_usd.toFixed(2)}`,
sla: data.sla_breached ? 'BREACHED' : 'SAFE',
outcome: data.outcome
}
}});
})
.catch(() => dispatch({ type: 'SET_COUNTERFACTUAL', payload: event.payload }));
break;
case 'reward':
dispatch({ type: 'ADD_REWARD', payload: event.payload });
break;
default:
break;
}
}