ashishMenon05 commited on
Commit
7fbbd40
·
1 Parent(s): 174293c

feat(ui): support unlimited agents with scalable terminal grid and scrollable settings

Browse files
backend/config.py CHANGED
@@ -20,31 +20,59 @@ class Settings:
20
  OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
21
  OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
22
 
23
- # AGENTS (Dynamic N-Agent Support)
24
  import json
25
- _default_agents = [
26
- {
27
- "id": "agent_a",
28
- "model": os.getenv("AGENT_A_MODEL", ""),
29
- "provider": os.getenv("AGENT_A_PROVIDER", "ollama"),
30
- "role": os.getenv("AGENT_A_ROLE", "INVESTIGATOR"),
31
- "system_prompt": os.getenv("AGENT_A_SYSTEM_PROMPT", ""),
32
- "temperature": float(os.getenv("AGENT_A_TEMPERATURE", "0.8"))
33
- },
34
- {
35
- "id": "agent_b",
36
- "model": os.getenv("AGENT_B_MODEL", ""),
37
- "provider": os.getenv("AGENT_B_PROVIDER", "ollama"),
38
- "role": os.getenv("AGENT_B_ROLE", "VALIDATOR"),
39
- "system_prompt": os.getenv("AGENT_B_SYSTEM_PROMPT", ""),
40
- "temperature": float(os.getenv("AGENT_B_TEMPERATURE", "0.6"))
41
- }
42
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  try:
44
  AGENTS_JSON = os.getenv("AGENTS_JSON")
45
- AGENTS = json.loads(AGENTS_JSON) if AGENTS_JSON else _default_agents
46
  except:
47
- AGENTS = _default_agents
48
  # EXECUTION ENVIRONMENT
49
  EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
50
  SSH_HOST = os.getenv("SSH_HOST", "")
 
20
  OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434/v1")
21
  OLLAMA_API_KEY = os.getenv("OLLAMA_API_KEY", "ollama")
22
 
23
+ # AGENTS (Dynamic N-Agent Support - supports unlimited agents)
24
  import json
25
+ _built_in_roles = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER", "SYSTEM_ADMIN", "SECURITY_ARCHITECT", "COMPLIANCE_OFFICER"]
26
+ _default_roles = ["INVESTIGATOR", "VALIDATOR", "FORENSIC_ANALYST", "NETWORK_ENGINEER"]
27
+
28
+ def _build_agents_from_env():
29
+ agents = []
30
+ suffix_map = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5, 'g': 6, 'h': 7, 'i': 8, 'j': 9}
31
+ for suffix, idx in suffix_map.items():
32
+ model_key = f"AGENT_{suffix.upper()}_MODEL"
33
+ provider_key = f"AGENT_{suffix.upper()}_PROVIDER"
34
+ role_key = f"AGENT_{suffix.upper()}_ROLE"
35
+ prompt_key = f"AGENT_{suffix.upper()}_SYSTEM_PROMPT"
36
+ temp_key = f"AGENT_{suffix.upper()}_TEMPERATURE"
37
+
38
+ model = os.getenv(model_key, "")
39
+ if model:
40
+ role_idx = idx % len(_default_roles)
41
+ agents.append({
42
+ "id": f"agent_{suffix}",
43
+ "model": model,
44
+ "provider": os.getenv(provider_key, "ollama"),
45
+ "role": os.getenv(role_key, _default_roles[role_idx]),
46
+ "system_prompt": os.getenv(prompt_key, ""),
47
+ "temperature": float(os.getenv(temp_key, str(0.7 - idx * 0.05)))
48
+ })
49
+
50
+ if not agents:
51
+ agents = [
52
+ {
53
+ "id": "agent_a",
54
+ "model": os.getenv("AGENT_A_MODEL", "meta-llama/Llama-3.1-8B-Instruct"),
55
+ "provider": os.getenv("AGENT_A_PROVIDER", "hf"),
56
+ "role": "INVESTIGATOR",
57
+ "system_prompt": "",
58
+ "temperature": 0.7
59
+ },
60
+ {
61
+ "id": "agent_b",
62
+ "model": os.getenv("AGENT_B_MODEL", "meta-llama/Llama-3.2-1B-Instruct"),
63
+ "provider": os.getenv("AGENT_B_PROVIDER", "hf"),
64
+ "role": "VALIDATOR",
65
+ "system_prompt": "",
66
+ "temperature": 0.6
67
+ }
68
+ ]
69
+ return agents
70
+
71
  try:
72
  AGENTS_JSON = os.getenv("AGENTS_JSON")
73
+ AGENTS = json.loads(AGENTS_JSON) if AGENTS_JSON else _build_agents_from_env()
74
  except:
75
+ AGENTS = _build_agents_from_env()
76
  # EXECUTION ENVIRONMENT
77
  EXECUTION_MODE = os.getenv("EXECUTION_MODE", "simulated")
78
  SSH_HOST = os.getenv("SSH_HOST", "")
backend/core/agent_runner.py CHANGED
@@ -92,19 +92,33 @@ class AgentRunner:
92
  sys_prompt = behavior + "\n\n" + tool_rules
93
 
94
  context = f"Current incident: {scenario.get('description', '')}\n"
95
- if hasattr(episode_state, 'last_partner_message') and episode_state.last_partner_message:
96
- context += f"Partner's last message: {episode_state.last_partner_message}\n"
 
 
 
 
 
 
 
 
 
 
 
97
  if hasattr(episode_state, 'clues_found') and episode_state.clues_found:
98
- context += f"Clues found: {episode_state.clues_found}\n"
 
 
99
 
100
  messages = [{"role": "system", "content": sys_prompt}]
101
 
102
- if hasattr(episode_state, 'all_messages'):
103
- all_msgs = episode_state.all_messages[-3:]
104
- if all_msgs:
105
- context += "\nRecent history:\n"
106
- for m in all_msgs:
107
- context += f"- {m}\n"
 
108
 
109
  messages.append({"role": "user", "content": context})
110
 
 
92
  sys_prompt = behavior + "\n\n" + tool_rules
93
 
94
  context = f"Current incident: {scenario.get('description', '')}\n"
95
+
96
+ other_agents = [a["id"] for a in settings.AGENTS if a["id"] != agent_id]
97
+ if other_agents:
98
+ context += f"Other agents in this investigation: {', '.join(other_agents)}\n"
99
+
100
+ agent_configs = {a["id"]: a for a in settings.AGENTS}
101
+ for other_id in other_agents:
102
+ other_msgs = episode_state.messages_by_agent.get(other_id, [])
103
+ if other_msgs:
104
+ other_role = agent_configs.get(other_id, {}).get("role", "AGENT")
105
+ last_msg = other_msgs[-1] if other_msgs else ""
106
+ context += f"\n[{other_role}] {other_id}'s latest insight: {last_msg[:300]}...\n"
107
+
108
  if hasattr(episode_state, 'clues_found') and episode_state.clues_found:
109
+ context += f"\nClues discovered so far:\n"
110
+ for clue in episode_state.clues_found[-5:]:
111
+ context += f"- {clue[:200]}\n"
112
 
113
  messages = [{"role": "system", "content": sys_prompt}]
114
 
115
+ recent_msgs = episode_state.all_messages[-6:]
116
+ if recent_msgs:
117
+ context += "\nRecent conversation history:\n"
118
+ for i, m in enumerate(recent_msgs[-4:]):
119
+ if len(m) > 150:
120
+ m = m[:150] + "..."
121
+ context += f"- {m}\n"
122
 
123
  messages.append({"role": "user", "content": context})
124
 
default.env CHANGED
@@ -13,16 +13,28 @@ OPENAI_API_KEY=
13
  OPENAI_BASE_URL=https://api.openai.com/v1
14
 
15
  # AGENTS - HuggingFace models (work with HF Inference API)
 
 
16
  AGENT_A_MODEL=meta-llama/Llama-3.1-8B-Instruct
17
  AGENT_B_MODEL=meta-llama/Llama-3.2-1B-Instruct
 
 
18
  AGENT_A_PROVIDER=hf
19
  AGENT_B_PROVIDER=hf
 
 
20
  AGENT_A_ROLE=INVESTIGATOR
21
  AGENT_B_ROLE=VALIDATOR
 
 
22
  AGENT_A_TEMPERATURE=0.7
23
- AGENT_B_TEMPERATURE=0.5
 
 
24
  AGENT_A_MAX_TOKENS=512
25
  AGENT_B_MAX_TOKENS=512
 
 
26
 
27
  # EXECUTION ENVIRONMENT
28
  EXECUTION_MODE=simulated
 
13
  OPENAI_BASE_URL=https://api.openai.com/v1
14
 
15
  # AGENTS - HuggingFace models (work with HF Inference API)
16
+ # Supports agents a through j (10 agents max via env vars)
17
+ # Additional agents can be configured via AGENTS_JSON env var
18
  AGENT_A_MODEL=meta-llama/Llama-3.1-8B-Instruct
19
  AGENT_B_MODEL=meta-llama/Llama-3.2-1B-Instruct
20
+ AGENT_C_MODEL=
21
+ AGENT_D_MODEL=
22
  AGENT_A_PROVIDER=hf
23
  AGENT_B_PROVIDER=hf
24
+ AGENT_C_PROVIDER=hf
25
+ AGENT_D_PROVIDER=hf
26
  AGENT_A_ROLE=INVESTIGATOR
27
  AGENT_B_ROLE=VALIDATOR
28
+ AGENT_C_ROLE=FORENSIC_ANALYST
29
+ AGENT_D_ROLE=NETWORK_ENGINEER
30
  AGENT_A_TEMPERATURE=0.7
31
+ AGENT_B_TEMPERATURE=0.6
32
+ AGENT_C_TEMPERATURE=0.5
33
+ AGENT_D_TEMPERATURE=0.5
34
  AGENT_A_MAX_TOKENS=512
35
  AGENT_B_MAX_TOKENS=512
36
+ AGENT_C_MAX_TOKENS=512
37
+ AGENT_D_MAX_TOKENS=512
38
 
39
  # EXECUTION ENVIRONMENT
40
  EXECUTION_MODE=simulated
frontend/src/components/EpisodeEndOverlay.jsx CHANGED
@@ -6,10 +6,9 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
6
  const handleDownload = () => {
7
  if (!gameState) return;
8
 
9
- // Assemble the detailed incident report
10
  const sc = gameState.scenario || {};
11
- const agentA = gameState.agents?.agent_a?.messages || [];
12
- const agentB = gameState.agents?.agent_b?.messages || [];
13
 
14
  let report = `=================================================================\n`;
15
  report += ` NEXUS INCIDENT INVESTIGATION REPORT \n`;
@@ -20,7 +19,17 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
20
  report += `Domain: ${sc.domain || 'N/A'}\n`;
21
  report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
22
  report += `Final Grading Score: ${Number(gameState?.cumulativeReward || metrics?.score || 0).toFixed(4)} / 1.00\n`;
23
- report += `Total Steps: ${gameState?.step || metrics?.steps || 'N/A'}\n\n`;
 
 
 
 
 
 
 
 
 
 
24
 
25
  report += `[ STEP REWARDS ]\n`;
26
  if (gameState?.rewardHistory && gameState.rewardHistory.length > 0) {
@@ -52,13 +61,10 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
52
  report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
53
  report += `=================================================================\n\n`;
54
 
55
- // Interweave the messages to show the timeline (roughly)
56
- // Since we don't have exact timestamps, we'll just print Agent A then Agent B summary,
57
- // or just print all tools called and errors encountered.
58
  const allErrors = [];
59
  const allTools = [];
60
 
61
- [...agentA, ...agentB].forEach(msg => {
62
  if (msg.type === 'tool_call') {
63
  allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
64
  }
@@ -66,7 +72,6 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
66
  allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
67
  }
68
  if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
69
- // Catch strings that say error but were marked success true somehow
70
  allErrors.push(`- Log/Cmd Error: ${msg.result}`);
71
  }
72
  });
@@ -222,70 +227,42 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
222
  {/* Right Column: Agent Metrics */}
223
  <div className="space-y-6">
224
  <h3 className="font-mono text-[10px] text-outline tracking-widest uppercase mb-4">Agent Performance Breakdown</h3>
225
- {/* Agent A */}
226
- <div className="relative group">
227
- <div className="absolute -left-4 top-0 bottom-0 w-1 bg-primary shadow-[0_0_8px_rgba(0,212,255,0.4)]"></div>
228
- <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
229
- <div className="flex justify-between items-center">
230
- <span className="font-headline font-bold text-primary tracking-tighter uppercase">Agent_Alpha</span>
231
- <span className="font-mono text-[10px] text-primary/50">CYAN_PROTOCOL</span>
232
- </div>
233
- {(() => {
234
- const msgs = gameState?.agents?.agent_a?.messages || [];
235
- const msgCount = msgs.filter(m => m.type === 'message').length;
236
- const toolCount = msgs.filter(m => m.type === 'tool_call').length;
237
- const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
238
- return (
239
- <div className="grid grid-cols-3 gap-2 text-center">
240
- <div>
241
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
242
- <span className="font-headline text-lg font-medium text-primary">{msgCount}</span>
243
- </div>
244
- <div className="border-x border-white/5">
245
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
246
- <span className="font-headline text-lg font-medium text-primary">{toolCount}</span>
247
- </div>
248
- <div>
249
- <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
250
- <span className="font-headline text-lg font-medium text-primary">{errCount}</span>
251
- </div>
252
  </div>
253
- );
254
- })()}
255
- </div>
256
- </div>
257
- {/* Agent B */}
258
- <div className="relative group">
259
- <div className="absolute -left-4 top-0 bottom-0 w-1 bg-secondary shadow-[0_0_8px_rgba(221,183,255,0.4)]"></div>
260
- <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
261
- <div className="flex justify-between items-center">
262
- <span className="font-headline font-bold text-secondary tracking-tighter uppercase">Agent_Bravo</span>
263
- <span className="font-mono text-[10px] text-secondary/50">VIOLET_PROTOCOL</span>
264
- </div>
265
- {(() => {
266
- const msgs = gameState?.agents?.agent_b?.messages || [];
267
- const msgCount = msgs.filter(m => m.type === 'message').length;
268
- const toolCount = msgs.filter(m => m.type === 'tool_call').length;
269
- const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
270
- return (
271
  <div className="grid grid-cols-3 gap-2 text-center">
272
  <div>
273
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
274
- <span className="font-headline text-lg font-medium text-secondary">{msgCount}</span>
275
  </div>
276
  <div className="border-x border-white/5">
277
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
278
- <span className="font-headline text-lg font-medium text-secondary">{toolCount}</span>
279
  </div>
280
  <div>
281
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
282
- <span className="font-headline text-lg font-medium text-secondary">{errCount}</span>
283
  </div>
284
  </div>
285
- );
286
- })()}
287
- </div>
288
- </div>
289
  </div>
290
  </div>
291
 
@@ -320,39 +297,38 @@ const EpisodeEndOverlay = ({ isOpen, onClose, metrics, gameState }) => {
320
  );
321
  })()}
322
 
323
- {/* Dual Agent Final Verdict Panel */}
324
  {(() => {
325
- const msgsA = gameState?.agents?.agent_a?.messages || [];
326
- const msgsB = gameState?.agents?.agent_b?.messages || [];
327
-
328
- const textMsgsA = msgsA.filter(m => m.type === 'message');
329
- const textMsgsB = msgsB.filter(m => m.type === 'message');
330
-
331
- const lastMsgA = textMsgsA[textMsgsA.length - 1];
332
- const lastMsgB = textMsgsB[textMsgsB.length - 1];
333
-
334
- if (!lastMsgA && !lastMsgB) return null;
 
335
 
336
  return (
337
  <div className="px-8 pb-8">
338
  <div className="p-6 bg-surface-container-low/40 border border-white/10 rounded-lg">
339
  <h3 className="font-headline font-bold text-on-surface tracking-widest uppercase mb-4 flex items-center gap-2">
340
  <span className="material-symbols-outlined">gavel</span>
341
- Dual Agent Final Verdict
342
  </h3>
343
  <div className="space-y-4">
344
- {lastMsgA && (
345
- <div className="p-4 bg-primary/5 border-l-2 border-primary rounded-r">
346
- <span className="font-mono text-[10px] text-primary uppercase block mb-1 tracking-widest">Agent Alpha Conclusion</span>
347
- <p className="text-sm text-on-surface/90 leading-relaxed">{lastMsgA.content || lastMsgA.text || lastMsgA.message}</p>
348
- </div>
349
- )}
350
- {lastMsgB && (
351
- <div className="p-4 bg-secondary/5 border-l-2 border-secondary rounded-r">
352
- <span className="font-mono text-[10px] text-secondary uppercase block mb-1 tracking-widest">Agent Bravo Conclusion</span>
353
- <p className="text-sm text-on-surface/90 leading-relaxed">{lastMsgB.content || lastMsgB.text || lastMsgB.message}</p>
354
- </div>
355
- )}
356
  </div>
357
  </div>
358
  </div>
 
6
  const handleDownload = () => {
7
  if (!gameState) return;
8
 
 
9
  const sc = gameState.scenario || {};
10
+ const allAgents = gameState.agents || {};
11
+ const allMessages = Object.values(allAgents).flatMap(a => a?.messages || []);
12
 
13
  let report = `=================================================================\n`;
14
  report += ` NEXUS INCIDENT INVESTIGATION REPORT \n`;
 
19
  report += `Domain: ${sc.domain || 'N/A'}\n`;
20
  report += `Difficulty: ${sc.difficulty || 'N/A'}\n`;
21
  report += `Final Grading Score: ${Number(gameState?.cumulativeReward || metrics?.score || 0).toFixed(4)} / 1.00\n`;
22
+ report += `Total Steps: ${gameState?.step || metrics?.steps || 'N/A'}\n`;
23
+ report += `Active Agents: ${Object.keys(allAgents).length}\n\n`;
24
+
25
+ report += `[ AGENTS DEPLOYED ]\n`;
26
+ Object.entries(allAgents).forEach(([agentId, agentData], idx) => {
27
+ const msgs = agentData?.messages || [];
28
+ const msgCount = msgs.filter(m => m.type === 'message').length;
29
+ const toolCount = msgs.filter(m => m.type === 'tool_call').length;
30
+ report += `${idx + 1}. ${agentId}: ${msgCount} messages, ${toolCount} tool calls\n`;
31
+ });
32
+ report += `\n`;
33
 
34
  report += `[ STEP REWARDS ]\n`;
35
  if (gameState?.rewardHistory && gameState.rewardHistory.length > 0) {
 
61
  report += `[ INVESTIGATION LOG & DETAILED TRACE ]\n`;
62
  report += `=================================================================\n\n`;
63
 
 
 
 
64
  const allErrors = [];
65
  const allTools = [];
66
 
67
+ allMessages.forEach(msg => {
68
  if (msg.type === 'tool_call') {
69
  allTools.push(`- ${msg.tool_name}(${JSON.stringify(msg.params)})`);
70
  }
 
72
  allErrors.push(`- Error from ${msg.tool_name}: ${msg.result}`);
73
  }
74
  if (msg.type === 'tool_result' && msg.result?.toLowerCase().includes('error')) {
 
75
  allErrors.push(`- Log/Cmd Error: ${msg.result}`);
76
  }
77
  });
 
227
  {/* Right Column: Agent Metrics */}
228
  <div className="space-y-6">
229
  <h3 className="font-mono text-[10px] text-outline tracking-widest uppercase mb-4">Agent Performance Breakdown</h3>
230
+ {Object.entries(gameState?.agents || {}).map(([agentId, agentData], idx) => {
231
+ const colors = ['primary', 'secondary', 'tertiary', 'error', 'success'];
232
+ const color = colors[idx % colors.length];
233
+ const msgs = agentData?.messages || [];
234
+ const msgCount = msgs.filter(m => m.type === 'message').length;
235
+ const toolCount = msgs.filter(m => m.type === 'tool_call').length;
236
+ const errCount = msgs.filter(m => m.type === 'tool_result' && m.result?.toLowerCase().includes('error')).length;
237
+ const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO'];
238
+ const agentName = agentNames[idx % agentNames.length];
239
+
240
+ return (
241
+ <div key={agentId} className="relative group">
242
+ <div className={`absolute -left-4 top-0 bottom-0 w-1 bg-${color} shadow-[0_0_8px_rgba(var(--${color}),0.4)]`}></div>
243
+ <div className="bg-surface-container-low/40 p-5 space-y-4 border border-white/5 rounded-r-lg">
244
+ <div className="flex justify-between items-center">
245
+ <span className={`font-headline font-bold text-${color} tracking-tighter uppercase`}>Agent_{agentName}</span>
246
+ <span className={`font-mono text-[10px] text-${color}/50`}>{agentId.toUpperCase()}</span>
 
 
 
 
 
 
 
 
 
 
247
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  <div className="grid grid-cols-3 gap-2 text-center">
249
  <div>
250
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">chat</span> MSGS</span>
251
+ <span className={`font-headline text-lg font-medium text-${color}`}>{msgCount}</span>
252
  </div>
253
  <div className="border-x border-white/5">
254
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">build</span> TOOLS</span>
255
+ <span className={`font-headline text-lg font-medium text-${color}`}>{toolCount}</span>
256
  </div>
257
  <div>
258
  <span className="font-mono text-[9px] text-outline flex flex-col items-center justify-center gap-1 uppercase"><span className="material-symbols-outlined text-[12px]">warning</span> ERRS</span>
259
+ <span className={`font-headline text-lg font-medium text-${color}`}>{errCount}</span>
260
  </div>
261
  </div>
262
+ </div>
263
+ </div>
264
+ );
265
+ })}
266
  </div>
267
  </div>
268
 
 
297
  );
298
  })()}
299
 
300
+ {/* Multi-Agent Final Verdict Panel */}
301
  {(() => {
302
+ const allAgents = gameState?.agents || {};
303
+ const agentEntries = Object.entries(allAgents);
304
+ const conclusions = agentEntries.map(([agentId, agentData]) => {
305
+ const msgs = agentData?.messages || [];
306
+ const textMsgs = msgs.filter(m => m.type === 'message');
307
+ return { agentId, lastMsg: textMsgs[textMsgs.length - 1] };
308
+ }).filter(c => c.lastMsg);
309
+
310
+ if (conclusions.length === 0) return null;
311
+
312
+ const colors = ['primary', 'secondary', 'tertiary', 'error', 'success'];
313
 
314
  return (
315
  <div className="px-8 pb-8">
316
  <div className="p-6 bg-surface-container-low/40 border border-white/10 rounded-lg">
317
  <h3 className="font-headline font-bold text-on-surface tracking-widest uppercase mb-4 flex items-center gap-2">
318
  <span className="material-symbols-outlined">gavel</span>
319
+ Multi-Agent Final Verdict
320
  </h3>
321
  <div className="space-y-4">
322
+ {conclusions.map(({ agentId, lastMsg }, idx) => {
323
+ const color = colors[idx % colors.length];
324
+ const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO'];
325
+ return (
326
+ <div key={agentId} className={`p-4 bg-${color}/5 border-l-2 border-${color} rounded-r`}>
327
+ <span className={`font-mono text-[10px] text-${color} uppercase block mb-1 tracking-widest`}>Agent {agentNames[idx % agentNames.length]} ({agentId}) Conclusion</span>
328
+ <p className="text-sm text-on-surface/90 leading-relaxed">{lastMsg.content || lastMsg.text || lastMsg.message}</p>
329
+ </div>
330
+ );
331
+ })}
 
 
332
  </div>
333
  </div>
334
  </div>
frontend/src/hooks/useWebSocket.js CHANGED
@@ -9,8 +9,6 @@ const useWebSocket = (url) => {
9
  step: 0,
10
  reward: 0,
11
  cumulativeReward: 0,
12
- agent_a_model: '',
13
- agent_b_model: '',
14
  agents: {},
15
  clues_found: [],
16
  rewardBreakdown: {},
 
9
  step: 0,
10
  reward: 0,
11
  cumulativeReward: 0,
 
 
12
  agents: {},
13
  clues_found: [],
14
  rewardBreakdown: {},
frontend/src/views/DashboardView.jsx CHANGED
@@ -141,9 +141,7 @@ const DashboardView = () => {
141
  active: false,
142
  step: 0,
143
  cumulativeReward: 0,
144
- messages_by_agent: {
145
- agent_a: []
146
- }
147
  };
148
 
149
  const sc = state.scenario || {};
@@ -219,20 +217,31 @@ const DashboardView = () => {
219
  </div>
220
 
221
  {/* N-Agent Terminals */}
222
- <div className={`grid grid-cols-1 ${configModels.agents.length > 2 ? 'lg:grid-cols-3' : 'lg:grid-cols-2'} gap-6`}>
 
 
 
 
 
 
223
  {configModels.agents.map((agent, index) => {
224
- const isPrimary = index % 2 === 0;
225
- const accentColor = isPrimary ? 'cyan' : 'purple';
226
- // We don't have agent specific status tracked deeply beyond STANDBY/ACTIVE globally right now
227
- // We deduce messages from state.agents
228
  const messages = state.agents?.[agent.id]?.messages || [];
229
  const agentStatus = state.active ? 'ACTIVE' : 'STANDBY';
230
- const icon = agent.role.includes('VALIDATOR') ? 'verified_user' : 'search';
 
 
 
 
 
 
 
231
 
232
  return (
233
  <AgentTerminal
234
  key={agent.id}
235
- agentName={`Agent ${String.fromCharCode(65 + index)}: ${agent.role.replace(/_/g, ' ')}`}
236
  model={agent.model}
237
  status={agentStatus}
238
  accentColor={accentColor}
@@ -322,11 +331,10 @@ const DashboardView = () => {
322
  onClose={() => setIsOverlayDismissed(true)}
323
  metrics={{
324
  score: Number(state.cumulativeReward || 0).toFixed(2),
325
- runtime: '00:00:00', // could calculate if we tracked start/end time
326
  steps: state.step || 0,
327
  rootCause: 'VERIFIED',
328
- agentA: { accuracy: 'High', latency: '42ms', iops: '9' },
329
- agentB: { accuracy: 'High', latency: '38ms', iops: '7' }
330
  }}
331
  gameState={state}
332
  />
 
141
  active: false,
142
  step: 0,
143
  cumulativeReward: 0,
144
+ agents: {}
 
 
145
  };
146
 
147
  const sc = state.scenario || {};
 
217
  </div>
218
 
219
  {/* N-Agent Terminals */}
220
+ <div className={`grid grid-cols-1 ${
221
+ configModels.agents.length === 1 ? 'lg:grid-cols-1' :
222
+ configModels.agents.length === 2 ? 'lg:grid-cols-2' :
223
+ configModels.agents.length === 3 ? 'lg:grid-cols-3' :
224
+ configModels.agents.length === 4 ? 'lg:grid-cols-2' :
225
+ configModels.agents.length <= 6 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
226
+ } gap-6 overflow-y-auto max-h-[80vh] custom-scrollbar pr-2`}>
227
  {configModels.agents.map((agent, index) => {
228
+ const colors = ['cyan', 'purple', 'green', 'orange', 'pink', 'yellow', 'red', 'blue'];
229
+ const accentColor = colors[index % colors.length];
 
 
230
  const messages = state.agents?.[agent.id]?.messages || [];
231
  const agentStatus = state.active ? 'ACTIVE' : 'STANDBY';
232
+ const icon = agent.role?.includes('VALIDATOR') ? 'verified_user' : 'search';
233
+
234
+ // Scalable naming: ALPHA, BRAVO... then AA, AB...
235
+ const agentNames = ['ALPHA', 'BRAVO', 'CHARLIE', 'DELTA', 'ECHO', 'FOXTROT', 'GOLF', 'HOTEL', 'INDIA', 'JULIETT', 'KILO', 'LIMA', 'MIKE', 'NOVEMBER', 'OSCAR', 'PAPA', 'QUEBEC', 'ROMEO', 'SIERRA', 'TANGO', 'UNIFORM', 'VICTOR', 'WHISKEY', 'X-RAY', 'YANKEE', 'ZULU'];
236
+ let name = agentNames[index % agentNames.length];
237
+ if (index >= agentNames.length) {
238
+ name = `${name}_${Math.floor(index / agentNames.length)}`;
239
+ }
240
 
241
  return (
242
  <AgentTerminal
243
  key={agent.id}
244
+ agentName={`Agent ${name}: ${agent.role?.replace(/_/g, ' ') || 'AGENT'}`}
245
  model={agent.model}
246
  status={agentStatus}
247
  accentColor={accentColor}
 
331
  onClose={() => setIsOverlayDismissed(true)}
332
  metrics={{
333
  score: Number(state.cumulativeReward || 0).toFixed(2),
334
+ runtime: '00:00:00',
335
  steps: state.step || 0,
336
  rootCause: 'VERIFIED',
337
+ agentCount: Object.keys(state.agents || {}).length
 
338
  }}
339
  gameState={state}
340
  />
frontend/src/views/SettingsView.jsx CHANGED
@@ -279,16 +279,17 @@ const SettingsView = () => {
279
  };
280
 
281
  const addAgent = () => {
282
- if (agents.length >= 4) return;
283
- const newId = `agent_${String.fromCharCode(97 + agents.length)}`;
 
284
  setAgents(prev => [...prev, {
285
- id: newId, provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.2-1B-Instruct', openaiModel: 'gpt-4o-mini', temp: 0.5, role: 'INVESTIGATOR', customRoleName: '', customPrompt: ''
286
  }]);
287
  };
288
 
289
  const removeAgent = (index) => {
290
  if (agents.length <= 1) return;
291
- setAgents(prev => prev.filter((_, i) => i !== index).map((a, i) => ({ ...a, id: `agent_${String.fromCharCode(97 + i)}` })));
292
  };
293
 
294
  const ProviderToggle = ({ agent, index }) => (
@@ -322,9 +323,14 @@ const SettingsView = () => {
322
  </div>
323
  </section>
324
 
325
- <div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch">
326
- {/* N-Agents Render */}
327
- {agents.map((agent, index) => {
 
 
 
 
 
328
  const isPrimary = index % 2 === 0;
329
  const accentColor = isPrimary ? 'primary' : 'secondary';
330
  const titleColor = isPrimary ? 'text-primary' : 'text-secondary';
@@ -340,8 +346,8 @@ const SettingsView = () => {
340
  <div className="flex-1">
341
  <div className="flex justify-between items-start">
342
  <div>
343
- <h3 className="font-headline text-xl font-bold uppercase">Agent {String.fromCharCode(65 + index)} <span className={`${titleColor} text-sm ml-2 tracking-tighter`}>[NPU_0{index + 1}]</span></h3>
344
- <p className="font-mono text-[10px] text-slate-500 uppercase">Process ID: {agent.id}</p>
345
  </div>
346
  <div className="flex gap-2 items-center">
347
  <ProviderToggle agent={agent} index={index} />
@@ -443,14 +449,15 @@ const SettingsView = () => {
443
  </div>
444
  );
445
  })}
446
- {agents.length < 4 && (
447
- <div className="md:col-span-12 flex justify-center mt-4">
448
- <button onClick={addAgent} className="flex items-center gap-2 px-8 py-3 rounded-xl border border-dashed border-outline-variant/30 text-outline-variant font-mono text-xs uppercase hover:bg-surface-container-highest hover:text-white transition-all">
449
- <span className="material-symbols-outlined text-[16px]">add</span>
450
- <span>Add Compute Node (Max 4)</span>
451
- </button>
452
- </div>
453
- )}
 
454
 
455
  {/* Execution Environment */}
456
  <div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
@@ -593,7 +600,7 @@ const SettingsView = () => {
593
  <div className="flex items-center gap-4">
594
  <button
595
  onClick={() => {
596
- setAgents([{ id: 'agent_a', provider: 'ollama', model: '', temp: 0.7, role: 'INVESTIGATOR' }]);
597
  setMaxSteps(12);
598
  }}
599
  className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"
 
279
  };
280
 
281
  const addAgent = () => {
282
+ const newId = `agent_${Date.now()}`;
283
+ const roles = ['INVESTIGATOR', 'VALIDATOR', 'FORENSIC_ANALYST', 'NETWORK_ENGINEER', 'SYSTEM_ADMIN', 'SECURITY_ARCHITECT', 'COMPLIANCE_OFFICER'];
284
+ const role = roles[agents.length % roles.length];
285
  setAgents(prev => [...prev, {
286
+ id: newId, provider: 'hf', model: '', hfModel: 'meta-llama/Llama-3.2-1B-Instruct', openaiModel: 'gpt-4o-mini', temp: Math.max(0.3, 0.7 - agents.length * 0.05), role, customRoleName: '', customPrompt: ''
287
  }]);
288
  };
289
 
290
  const removeAgent = (index) => {
291
  if (agents.length <= 1) return;
292
+ setAgents(prev => prev.filter((_, i) => i !== index));
293
  };
294
 
295
  const ProviderToggle = ({ agent, index }) => (
 
323
  </div>
324
  </section>
325
 
326
+ <div className="md:col-span-12">
327
+ <div className="flex items-center gap-3 mb-4">
328
+ <span className="font-mono text-[10px] tracking-widest text-primary uppercase">Active_Agent_Nodes</span>
329
+ <div className="flex-1 h-px bg-primary/10"></div>
330
+ </div>
331
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-stretch max-h-[1200px] overflow-y-auto pr-2 custom-scrollbar p-1">
332
+ {/* N-Agents Render */}
333
+ {agents.map((agent, index) => {
334
  const isPrimary = index % 2 === 0;
335
  const accentColor = isPrimary ? 'primary' : 'secondary';
336
  const titleColor = isPrimary ? 'text-primary' : 'text-secondary';
 
346
  <div className="flex-1">
347
  <div className="flex justify-between items-start">
348
  <div>
349
+ <h3 className="font-headline text-xl font-bold uppercase">{agent.role.replace(/_/g, ' ')} <span className={`${titleColor} text-sm ml-2 tracking-tighter`}>[{agent.id.toUpperCase()}]</span></h3>
350
+ <p className="font-mono text-[10px] text-slate-500 uppercase">Node ID: {agent.id}</p>
351
  </div>
352
  <div className="flex gap-2 items-center">
353
  <ProviderToggle agent={agent} index={index} />
 
449
  </div>
450
  );
451
  })}
452
+ </div>
453
+ </div>
454
+
455
+ <div className="md:col-span-12 flex justify-center mt-4">
456
+ <button onClick={addAgent} className="flex items-center gap-2 px-8 py-3 rounded-xl border border-dashed border-outline-variant/30 text-outline-variant font-mono text-xs uppercase hover:bg-surface-container-highest hover:text-white transition-all bg-white/5">
457
+ <span className="material-symbols-outlined text-[16px]">add</span>
458
+ <span>Add Agent Node</span>
459
+ </button>
460
+ </div>
461
 
462
  {/* Execution Environment */}
463
  <div className="md:col-span-12 glass-panel rounded-xl p-8 refractive-edge">
 
600
  <div className="flex items-center gap-4">
601
  <button
602
  onClick={() => {
603
+ setAgents([{ id: 'agent_default', provider: 'ollama', model: '', temp: 0.7, role: 'INVESTIGATOR', customRoleName: '', customPrompt: '' }]);
604
  setMaxSteps(12);
605
  }}
606
  className="px-8 py-3 bg-surface-container-high text-on-surface-variant font-headline font-bold text-sm tracking-widest rounded hover:bg-surface-container-highest hover:text-white transition-all uppercase"