feat: Phase 2 — Intel Protocol + Iron Protocol

Arc Reactor v2.0:
- research handler: DDG search → async page fetch → trafilatura extraction → Claude synthesis
- tool_loop handler: multi-step agent loop (up to 200 iter) with web_search, fetch_url, jarvis_agents, jarvis_alerts, current_time tools
- llm handler: multi-provider router (Claude/Groq/Ollama)
- /jobs/recent endpoint for HUD polling
- Phase 1 handlers preserved (ping/echo/shell)

chat.php — Tier 0.9 Intel Protocol (before KB intent engine):
- Detects: research/investigate/deep-dive/look up/find out about → research job
- Detects: step-by-step/figure out/analyze and report → tool_loop job
- Returns arc_job ID in response for UI polling
- Depth modifiers: quick/standard/deep

index.html:
- INTEL tab in right panel tab bar
- Research result cards with expand/collapse, synthesis, sources, status
- Live polling (4s) when INTEL tab is active + active jobs present
- Auto-switches to INTEL tab when research is triggered from chat
- intelPrompt() pre-fills chat input for new research

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 04:16:29 +00:00
parent 7013a80428
commit 9ea43c852b
2 changed files with 219 additions and 0 deletions
+63
View File
@@ -1063,6 +1063,67 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e
}
}
// ── Tier 0.9: Intel Protocol — research & tool_loop detection ────────────
$arcJobId = null;
// Detect "research X", "look up X", "deep dive X", "investigate X", "find out about X"
$intelPatterns = [
'/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research',
'/^(?:jarvis[,\s]+)?(?:look\s+(?:up|into)|find\s+out\s+(?:about)?)\s+(.+)/i' => 'research',
'/^(?:jarvis[,\s]+)?(?:step[- ]by[- ]step|figure\s+out|analyze\s+and\s+report|work\s+through)\s+(.+)/i' => 'tool_loop',
'/^(?:jarvis[,\s]+)?(?:run\s+a\s+research\s+(?:job|task)\s+on)\s+(.+)/i' => 'research',
];
if (!$reply) {
foreach ($intelPatterns as $pattern => $jobType) {
if (preg_match($pattern, $message, $m)) {
$queryOrTask = trim($m[1]);
if (strlen($queryOrTask) < 3) break;
$depth = 'standard';
if (preg_match('/\b(?:quick|brief)\b/i', $message)) $depth = 'quick';
if (preg_match('/\b(?:deep|thorough|comprehensive|full)\b/i', $message)) $depth = 'deep';
$jobPayload = $jobType === 'research'
? ['query' => $queryOrTask, 'depth' => $depth, 'provider' => 'claude']
: ['task' => $queryOrTask, 'max_iterations' => 12, 'provider' => 'claude'];
// Submit to Arc Reactor
$arcCh = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($arcCh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'type' => $jobType,
'payload' => $jobPayload,
'priority' => 7,
'created_by' => 'chat:' . $sessionId,
]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$arcRes = json_decode(curl_exec($arcCh), true);
curl_close($arcCh);
if (isset($arcRes['job_id'])) {
$arcJobId = $arcRes['job_id'];
if ($jobType === 'research') {
$depthLabel = strtoupper($depth);
$reply = "◈ INTEL PROTOCOL ACTIVATED — Running {$depthLabel} research on **{$queryOrTask}** (Job #{$arcJobId}). I'm searching sources, extracting content, and synthesizing a briefing now, {$userAddr}. Switch to the INTEL tab to watch live progress.";
} else {
$reply = "◈ IRON PROTOCOL ACTIVATED — Multi-step analysis initiated for **{$queryOrTask}** (Job #{$arcJobId}). I'll work through this systematically using available tools, {$userAddr}. Results will appear in the INTEL tab.";
}
$source = "arc:{$jobType}";
} else {
$reply = "Intel Protocol is offline, {$userAddr}. Arc Reactor may be unavailable — I'll try to answer directly.";
$source = 'arc:offline';
}
break;
}
}
}
// ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
if (!$reply) {
$matched = KBEngine::match($message);
@@ -1193,6 +1254,7 @@ if (!$reply) {
}
}
// ── Tier 2: Ollama local LLM (fast local fallback) ───────────────────────
if (!$reply && defined('OLLAMA_HOST') && OLLAMA_HOST) {
$ollamaHost = OLLAMA_HOST;
@@ -1413,4 +1475,5 @@ echo json_encode([
'source' => $source,
'session_id' => $sessionId,
'timestamp' => date('c'),
'arc_job' => $arcJobId,
]);
+156
View File
@@ -875,6 +875,27 @@ body::after{
transition:filter 1.2s ease;
}
#app.sleeping #sleepOverlay{display:flex}
/* ── INTEL PROTOCOL — research result cards ──────────────────────── */
.intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden}
.intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
.intel-card-head:hover{background:rgba(0,212,255,0.06)}
.intel-card-query{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.intel-card-status{font-family:var(--font-mono);font-size:0.55rem;padding:2px 6px;border-radius:2px;flex-shrink:0}
.intel-card-status.running{color:#ffd700;border:1px solid rgba(255,215,0,0.4);animation:pulse 1.5s ease-in-out infinite}
.intel-card-status.done{color:var(--green);border:1px solid rgba(0,255,136,0.3)}
.intel-card-status.failed{color:var(--red);border:1px solid rgba(255,34,68,0.3)}
.intel-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)}
.intel-card.open .intel-card-body{display:block}
.intel-card-body .synthesis{font-size:0.65rem;line-height:1.6;color:var(--text);margin:8px 0;white-space:pre-wrap}
.intel-sources{margin-top:8px}
.intel-source{font-size:0.58rem;color:var(--text-dim);padding:2px 0;border-bottom:1px solid rgba(0,212,255,0.06)}
.intel-source a{color:var(--cyan2);text-decoration:none}
.intel-source a:hover{text-decoration:underline}
.intel-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px}
.intel-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;cursor:pointer;margin-bottom:8px}
.intel-new-btn:hover{background:rgba(0,212,255,0.12)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
</style>
</head>
<body>
@@ -1077,6 +1098,7 @@ body::after{
<div class="tab" onclick="switchTab('news')">NEWS</div>
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
<div class="tab" onclick="switchTab('sites')">SITES</div>
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
</div>
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
<div id="vm-list"><div class="loading-shimmer"></div></div>
@@ -1093,6 +1115,9 @@ body::after{
<div id="tab-agents" class="tab-pane" style="overflow-y:auto;flex:1">
<div id="agents-list"><div class="loading-shimmer"></div></div>
</div>
<div id="tab-intel" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
<div id="intel-list"><div class="loading-shimmer"></div></div>
</div>
</div>
</div>
@@ -2997,6 +3022,7 @@ function switchTab(name) {
if (pane) pane.classList.add('active');
if (name === 'news') loadNews();
if (name === 'agents') loadAgents();
if (name === 'intel') loadIntel();
if (name === 'alerts') loadAlerts();
}
@@ -3140,6 +3166,7 @@ async function sendMessage() {
addMessage('jarvis', data.reply);
speak(data.reply);
}
if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); }
} catch(e) {
const bubble = document.getElementById('thinking-bubble');
if (bubble) bubble.remove();
@@ -3497,6 +3524,135 @@ async function arcWaitJob(jobId, onProgress) {
}
// ── INTEL PROTOCOL — HUD panel ────────────────────────────────────────
let _intelPollTimer = null;
let _intelActiveJobs = new Set();
let _intelLastLoad = 0;
async function loadIntel() {
const el = document.getElementById('intel-list');
if (!el) return;
_intelLastLoad = Date.now();
try {
// Fetch recent research + tool_loop jobs
const [resJobs, toolJobs] = await Promise.all([
api('arc?action=jobs&status=&limit=20').catch(() => []),
Promise.resolve([]),
]);
const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : [];
if (!jobs.length) {
el.innerHTML = '<div class="intel-empty">◈ NO INTEL JOBS<br><span style="opacity:0.5">Say "research [topic]" to activate</span></div>';
stopIntelPolling();
return;
}
// Check for active jobs
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
if (hasActive) startIntelPolling(); else stopIntelPolling();
let html = '<button class="intel-new-btn" onclick="intelPrompt()">⚡ NEW RESEARCH</button>';
for (const job of jobs) {
const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running';
const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running';
const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase();
const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM';
// Get result details if done
let bodyHtml = '';
if (job.status === 'done' && job.result) {
let r = job.result;
if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} }
if (typeof r === 'object') {
const synthesis = (r.synthesis || r.result || r.response || '').trim();
const sources = r.sources || [];
const query = r.query || r.task || '';
const provider = r.provider || '';
bodyHtml = `<div class="intel-card-body">`;
if (provider) bodyHtml += `<div style="font-size:0.55rem;color:var(--text-dim);margin:6px 0 2px;font-family:var(--font-mono)">PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}</div>`;
if (synthesis) bodyHtml += `<div class="synthesis">${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'
[...truncated — view in admin]':''}</div>`;
if (sources.length) {
bodyHtml += '<div class="intel-sources"><div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:4px;font-family:var(--font-display)">SOURCES</div>';
sources.slice(0,5).forEach((s,i) => {
const title = escHtml((s.title||s.url||'').substring(0,60));
const url = escHtml(s.url||'');
bodyHtml += `<div class="intel-source">${i+1}. <a href="${url}" target="_blank" rel="noopener">${title||url}</a></div>`;
});
bodyHtml += '</div>';
}
bodyHtml += '</div>';
}
} else if (job.status === 'running' || job.status === 'queued') {
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--text-dim);padding:8px 0;font-family:var(--font-mono)">${typeMsg}</div></div>`;
} else if (job.status === 'failed' && job.error) {
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--red);padding:8px 0;font-family:var(--font-mono)">${escHtml(job.error.substring(0,200))}</div></div>`;
}
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
html += `<div class="intel-card${(isOpen && bodyHtml) ? ' open':''}" id="intel-card-${job.id}">
<div class="intel-card-head" onclick="toggleIntelCard(${job.id})">
<span style="font-size:0.55rem;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0">${typeLabel}</span>
<span class="intel-card-query">#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}</span>
<span style="font-size:0.55rem;color:var(--text-dim);flex-shrink:0;font-family:var(--font-mono)">${ts}</span>
<span class="intel-card-status ${statusClass}">${statusLabel}</span>
</div>
${bodyHtml}
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="intel-empty">INTEL OFFLINE</div>';
}
}
function toggleIntelCard(id) {
const card = document.getElementById('intel-card-' + id);
if (!card) return;
if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id);
else _intelActiveJobs.add(id);
card.classList.toggle('open');
}
function startIntelPolling() {
if (_intelPollTimer) return;
_intelPollTimer = setInterval(() => {
if (document.getElementById('tab-intel')?.classList.contains('active')) {
loadIntel();
}
}, 4000);
}
function stopIntelPolling() {
if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; }
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function intelPrompt() {
const input = document.getElementById('textInput');
if (input) { input.value = 'research '; input.focus(); }
}
// Called from chat.js when arc_job is returned
function onArcJobStarted(jobId, jobType) {
_intelActiveJobs.add(jobId);
// Auto-switch to INTEL tab
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
if (intelTab) intelTab.click();
startIntelPolling();
}
async function loadAgents() {
const [listData, metricsData] = await Promise.all([
api('agent/list'),