Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
4488847c13 feat: Add Reasoning Trace HUD Component
Some checks failed
CI / test (pull_request) Failing after 57s
CI / validate (pull_request) Failing after 34s
Review Approval Gate / verify-review (pull_request) Failing after 6s
Closes #875

- Added new ReasoningTrace component for real-time reasoning visualization
- Shows agent's reasoning steps during complex task execution
- Supports step types: THINK, DECIDE, RECALL, PLAN, EXECUTE, VERIFY, DOUBT, MEMORY
- Includes confidence visualization, task tracking, and export functionality
- Integrated into existing GOFAI HUD system
2026-04-13 18:23:05 -04:00
6 changed files with 718 additions and 9 deletions

2
app.js
View File

@@ -9,6 +9,7 @@ import { MemoryBirth } from './nexus/components/memory-birth.js';
import { MemoryOptimizer } from './nexus/components/memory-optimizer.js';
import { MemoryInspect } from './nexus/components/memory-inspect.js';
import { MemoryPulse } from './nexus/components/memory-pulse.js';
import { ReasoningTrace } from './nexus/components/reasoning-trace.js';
// ═══════════════════════════════════════════
// NEXUS v1.1 — Portal System Update
@@ -758,6 +759,7 @@ async function init() {
SpatialAudio.bindSpatialMemory(SpatialMemory);
MemoryInspect.init({ onNavigate: _navigateToMemory });
MemoryPulse.init(SpatialMemory);
ReasoningTrace.init();
updateLoad(90);
loadSession();

View File

@@ -101,6 +101,19 @@
<div class="panel-header">ADAPTIVE CALIBRATOR</div>
<div id="calibrator-log-content" class="panel-content"></div>
</div>
<div class="hud-panel" id="reasoning-trace">
<div class="trace-header-container">
<div class="panel-header"><span class="trace-icon">🧠</span> REASONING TRACE</div>
<div class="trace-controls">
<button class="trace-btn" id="trace-clear" title="Clear trace">🗑️</button>
<button class="trace-btn" id="trace-toggle" title="Toggle visibility">👁️</button>
<button class="trace-btn" id="trace-export" title="Export trace">📤</button>
</div>
</div>
<div class="trace-task" id="trace-task">No active task</div>
<div class="trace-counter" id="trace-counter">0 steps</div>
<div id="reasoning-trace-content" class="panel-content trace-content"></div>
</div>
</div>
<!-- Evennia Room Snapshot Panel -->

View File

@@ -2880,7 +2880,7 @@ def main():
# Start world tick system
world_tick_system.start()
server = ThreadingHTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server = HTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server.serve_forever()

View File

@@ -0,0 +1,451 @@
// ═══════════════════════════════════════════════════
// REASONING TRACE HUD COMPONENT
// ═══════════════════════════════════════════════════
//
// Displays a real-time trace of the agent's reasoning
// steps during complex task execution. Shows the chain
// of thought, decision points, and confidence levels.
//
// Usage:
// ReasoningTrace.init();
// ReasoningTrace.addStep(step);
// ReasoningTrace.clear();
// ReasoningTrace.toggle();
// ═══════════════════════════════════════════════════
const ReasoningTrace = (() => {
// ── State ─────────────────────────────────────────
let _container = null;
let _content = null;
let _header = null;
let _steps = [];
let _maxSteps = 20;
let _isVisible = true;
let _currentTask = null;
let _stepCounter = 0;
// ── Config ────────────────────────────────────────
const STEP_TYPES = {
THINK: { icon: '💭', color: '#4af0c0', label: 'THINK' },
DECIDE: { icon: '⚖️', color: '#ffd700', label: 'DECIDE' },
RECALL: { icon: '🔍', color: '#7b5cff', label: 'RECALL' },
PLAN: { icon: '📋', color: '#ff8c42', label: 'PLAN' },
EXECUTE: { icon: '⚡', color: '#ff4466', label: 'EXECUTE' },
VERIFY: { icon: '✅', color: '#4af0c0', label: 'VERIFY' },
DOUBT: { icon: '❓', color: '#ff8c42', label: 'DOUBT' },
MEMORY: { icon: '💾', color: '#7b5cff', label: 'MEMORY' }
};
// ── Helpers ───────────────────────────────────────
function _escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function _formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function _getConfidenceBar(confidence) {
if (confidence === undefined || confidence === null) return '';
const percent = Math.max(0, Math.min(100, Math.round(confidence * 100)));
const bars = Math.round(percent / 10);
const filled = '█'.repeat(bars);
const empty = '░'.repeat(10 - bars);
return `<span class="confidence-bar" title="${percent}% confidence">${filled}${empty}</span>`;
}
// ── DOM Setup ─────────────────────────────────────
function _createDOM() {
// Create container if it doesn't exist
if (_container) return;
_container = document.createElement('div');
_container.id = 'reasoning-trace';
_container.className = 'hud-panel reasoning-trace';
_header = document.createElement('div');
_header.className = 'panel-header';
_header.innerHTML = `<span class="trace-icon">🧠</span> REASONING TRACE`;
// Task indicator
const taskIndicator = document.createElement('div');
taskIndicator.className = 'trace-task';
taskIndicator.id = 'trace-task';
taskIndicator.textContent = 'No active task';
// Step counter
const stepCounter = document.createElement('div');
stepCounter.className = 'trace-counter';
stepCounter.id = 'trace-counter';
stepCounter.textContent = '0 steps';
// Controls
const controls = document.createElement('div');
controls.className = 'trace-controls';
controls.innerHTML = `
<button class="trace-btn" id="trace-clear" title="Clear trace">🗑️</button>
<button class="trace-btn" id="trace-toggle" title="Toggle visibility">👁️</button>
<button class="trace-btn" id="trace-export" title="Export trace">📤</button>
`;
// Header container
const headerContainer = document.createElement('div');
headerContainer.className = 'trace-header-container';
headerContainer.appendChild(_header);
headerContainer.appendChild(controls);
// Content area
_content = document.createElement('div');
_content.className = 'panel-content trace-content';
_content.id = 'reasoning-trace-content';
// Assemble
_container.appendChild(headerContainer);
_container.appendChild(taskIndicator);
_container.appendChild(stepCounter);
_container.appendChild(_content);
// Add to HUD
const hud = document.getElementById('hud');
if (hud) {
const gofaiHud = hud.querySelector('.gofai-hud');
if (gofaiHud) {
gofaiHud.appendChild(_container);
} else {
hud.appendChild(_container);
}
}
// Add event listeners
document.getElementById('trace-clear')?.addEventListener('click', clear);
document.getElementById('trace-toggle')?.addEventListener('click', toggle);
document.getElementById('trace-export')?.addEventListener('click', exportTrace);
}
// ── Rendering ─────────────────────────────────────
function _renderStep(step, index) {
const typeConfig = STEP_TYPES[step.type] || STEP_TYPES.THINK;
const timestamp = _formatTimestamp(step.timestamp);
const confidence = _getConfidenceBar(step.confidence);
const stepEl = document.createElement('div');
stepEl.className = `trace-step trace-step-${step.type.toLowerCase()}`;
stepEl.dataset.stepId = step.id;
// Step header
const header = document.createElement('div');
header.className = 'trace-step-header';
header.innerHTML = `
<span class="step-icon">${typeConfig.icon}</span>
<span class="step-type" style="color: ${typeConfig.color}">${typeConfig.label}</span>
<span class="step-time">${timestamp}</span>
${confidence}
`;
// Step content
const content = document.createElement('div');
content.className = 'trace-step-content';
if (step.thought) {
const thought = document.createElement('div');
thought.className = 'step-thought';
thought.textContent = step.thought;
content.appendChild(thought);
}
if (step.reasoning) {
const reasoning = document.createElement('div');
reasoning.className = 'step-reasoning';
reasoning.textContent = step.reasoning;
content.appendChild(reasoning);
}
if (step.decision) {
const decision = document.createElement('div');
decision.className = 'step-decision';
decision.innerHTML = `<strong>Decision:</strong> ${_escapeHtml(step.decision)}`;
content.appendChild(decision);
}
if (step.alternatives && step.alternatives.length > 0) {
const alternatives = document.createElement('div');
alternatives.className = 'step-alternatives';
alternatives.innerHTML = `<strong>Alternatives:</strong> ${step.alternatives.map(a => _escapeHtml(a)).join(', ')}`;
content.appendChild(alternatives);
}
if (step.source) {
const source = document.createElement('div');
source.className = 'step-source';
source.innerHTML = `<strong>Source:</strong> ${_escapeHtml(step.source)}`;
content.appendChild(source);
}
stepEl.appendChild(header);
stepEl.appendChild(content);
return stepEl;
}
function _render() {
if (!_content) return;
// Clear content
_content.innerHTML = '';
// Update task indicator
const taskEl = document.getElementById('trace-task');
if (taskEl) {
taskEl.textContent = _currentTask || 'No active task';
taskEl.className = _currentTask ? 'trace-task active' : 'trace-task';
}
// Update step counter
const counterEl = document.getElementById('trace-counter');
if (counterEl) {
counterEl.textContent = `${_steps.length} step${_steps.length !== 1 ? 's' : ''}`;
}
// Render steps (newest first)
const sortedSteps = [..._steps].sort((a, b) => b.timestamp - a.timestamp);
for (let i = 0; i < sortedSteps.length; i++) {
const stepEl = _renderStep(sortedSteps[i], i);
_content.appendChild(stepEl);
// Add separator between steps
if (i < sortedSteps.length - 1) {
const separator = document.createElement('div');
separator.className = 'trace-separator';
_content.appendChild(separator);
}
}
// Show empty state if no steps
if (_steps.length === 0) {
const empty = document.createElement('div');
empty.className = 'trace-empty';
empty.innerHTML = `
<span class="empty-icon">💭</span>
<span class="empty-text">No reasoning steps yet</span>
<span class="empty-hint">Start a task to see the trace</span>
`;
_content.appendChild(empty);
}
}
// ── Public API ────────────────────────────────────
function init() {
_createDOM();
_render();
console.info('[ReasoningTrace] Initialized');
}
/**
* Add a reasoning step to the trace.
* @param {Object} step - The reasoning step
* @param {string} step.type - Step type (THINK, DECIDE, RECALL, PLAN, EXECUTE, VERIFY, DOUBT, MEMORY)
* @param {string} step.thought - The main thought/content
* @param {string} [step.reasoning] - Detailed reasoning
* @param {string} [step.decision] - Decision made
* @param {string[]} [step.alternatives] - Alternative options considered
* @param {string} [step.source] - Source of information
* @param {number} [step.confidence] - Confidence level (0-1)
* @param {string} [step.taskId] - Associated task ID
*/
function addStep(step) {
if (!step || !step.thought) return;
// Generate unique ID
const id = `step-${++_stepCounter}-${Date.now()}`;
// Create step object
const newStep = {
id,
timestamp: Date.now(),
type: step.type || 'THINK',
thought: step.thought,
reasoning: step.reasoning || null,
decision: step.decision || null,
alternatives: step.alternatives || null,
source: step.source || null,
confidence: step.confidence !== undefined ? Math.max(0, Math.min(1, step.confidence)) : null,
taskId: step.taskId || _currentTask
};
// Add to steps array
_steps.unshift(newStep);
// Limit number of steps
if (_steps.length > _maxSteps) {
_steps = _steps.slice(0, _maxSteps);
}
// Update task if provided
if (step.taskId && step.taskId !== _currentTask) {
setTask(step.taskId);
}
// Re-render
_render();
// Log to console for debugging
console.debug(`[ReasoningTrace] ${newStep.type}: ${newStep.thought}`);
return newStep.id;
}
/**
* Set the current task being traced.
* @param {string} taskId - Task identifier
*/
function setTask(taskId) {
_currentTask = taskId;
_render();
console.info(`[ReasoningTrace] Task set: ${taskId}`);
}
/**
* Clear all steps from the trace.
*/
function clear() {
_steps = [];
_stepCounter = 0;
_render();
console.info('[ReasoningTrace] Cleared');
}
/**
* Toggle the visibility of the trace panel.
*/
function toggle() {
_isVisible = !_isVisible;
if (_container) {
_container.style.display = _isVisible ? 'block' : 'none';
}
console.info(`[ReasoningTrace] Visibility: ${_isVisible ? 'shown' : 'hidden'}`);
}
/**
* Export the trace as JSON.
* @returns {string} JSON string of the trace
*/
function exportTrace() {
const exportData = {
task: _currentTask,
exportedAt: new Date().toISOString(),
steps: _steps.map(step => ({
type: step.type,
thought: step.thought,
reasoning: step.reasoning,
decision: step.decision,
alternatives: step.alternatives,
source: step.source,
confidence: step.confidence,
timestamp: new Date(step.timestamp).toISOString()
}))
};
const json = JSON.stringify(exportData, null, 2);
// Copy to clipboard
navigator.clipboard.writeText(json).then(() => {
console.info('[ReasoningTrace] Copied to clipboard');
// Show feedback
const btn = document.getElementById('trace-export');
if (btn) {
const original = btn.innerHTML;
btn.innerHTML = '✅';
setTimeout(() => { btn.innerHTML = original; }, 1000);
}
}).catch(err => {
console.error('[ReasoningTrace] Failed to copy:', err);
});
return json;
}
/**
* Get the current trace data.
* @returns {Object} Current trace state
*/
function getTrace() {
return {
task: _currentTask,
steps: [..._steps],
stepCount: _steps.length,
isVisible: _isVisible
};
}
/**
* Get steps filtered by type.
* @param {string} type - Step type to filter by
* @returns {Array} Filtered steps
*/
function getStepsByType(type) {
return _steps.filter(step => step.type === type);
}
/**
* Get steps for a specific task.
* @param {string} taskId - Task ID to filter by
* @returns {Array} Filtered steps
*/
function getStepsByTask(taskId) {
return _steps.filter(step => step.taskId === taskId);
}
/**
* Mark the current task as complete.
* @param {string} [result] - Optional result description
*/
function completeTask(result) {
if (_currentTask) {
addStep({
type: 'VERIFY',
thought: `Task completed: ${result || 'Success'}`,
taskId: _currentTask
});
// Clear current task after a delay
setTimeout(() => {
_currentTask = null;
_render();
}, 2000);
}
}
// ── Return Public API ─────────────────────────────
return {
init,
addStep,
setTask,
clear,
toggle,
exportTrace,
getTrace,
getStepsByType,
getStepsByTask,
completeTask,
STEP_TYPES
};
})();
export { ReasoningTrace };

249
style.css
View File

@@ -2685,3 +2685,252 @@ body.operator-mode #mode-label {
color: #ffd700;
}
/* ═══ REASONING TRACE COMPONENT ═══ */
.reasoning-trace {
width: 320px;
max-height: 400px;
display: flex;
flex-direction: column;
}
.trace-header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.trace-header-container .panel-header {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
.trace-icon {
margin-right: 4px;
}
.trace-controls {
display: flex;
gap: 4px;
}
.trace-btn {
background: rgba(74, 240, 192, 0.1);
border: 1px solid rgba(74, 240, 192, 0.2);
color: #4af0c0;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
border-radius: 2px;
transition: all 0.2s ease;
}
.trace-btn:hover {
background: rgba(74, 240, 192, 0.2);
border-color: #4af0c0;
}
.trace-task {
font-size: 9px;
color: #8899aa;
margin-bottom: 4px;
padding: 2px 6px;
background: rgba(0, 0, 0, 0.2);
border-radius: 2px;
font-family: 'JetBrains Mono', monospace;
}
.trace-task.active {
color: #4af0c0;
background: rgba(74, 240, 192, 0.1);
border-left: 2px solid #4af0c0;
}
.trace-counter {
font-size: 9px;
color: #667788;
margin-bottom: 6px;
font-family: 'JetBrains Mono', monospace;
}
.trace-content {
flex: 1;
overflow-y: auto;
max-height: 300px;
}
.trace-step {
margin-bottom: 8px;
padding: 6px;
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
border-left: 3px solid #4af0c0;
transition: all 0.2s ease;
}
.trace-step-think {
border-left-color: #4af0c0;
}
.trace-step-decide {
border-left-color: #ffd700;
}
.trace-step-recall {
border-left-color: #7b5cff;
}
.trace-step-plan {
border-left-color: #ff8c42;
}
.trace-step-execute {
border-left-color: #ff4466;
}
.trace-step-verify {
border-left-color: #4af0c0;
}
.trace-step-doubt {
border-left-color: #ff8c42;
}
.trace-step-memory {
border-left-color: #7b5cff;
}
.trace-step-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-size: 10px;
}
.step-icon {
font-size: 12px;
}
.step-type {
font-weight: 700;
letter-spacing: 0.5px;
font-family: 'JetBrains Mono', monospace;
}
.step-time {
color: #667788;
font-size: 9px;
margin-left: auto;
font-family: 'JetBrains Mono', monospace;
}
.confidence-bar {
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
color: #4af0c0;
letter-spacing: -1px;
margin-left: 4px;
}
.trace-step-content {
font-size: 11px;
line-height: 1.4;
color: #d9f7ff;
}
.step-thought {
margin-bottom: 4px;
font-style: italic;
color: #e0f0ff;
}
.step-reasoning {
margin-bottom: 4px;
color: #aabbcc;
font-size: 10px;
padding-left: 8px;
border-left: 1px solid rgba(74, 240, 192, 0.2);
}
.step-decision {
margin-bottom: 4px;
color: #ffd700;
font-size: 10px;
}
.step-alternatives {
margin-bottom: 4px;
color: #8899aa;
font-size: 10px;
}
.step-source {
margin-bottom: 4px;
color: #7b5cff;
font-size: 10px;
}
.trace-separator {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(74, 240, 192, 0.2), transparent);
margin: 6px 0;
}
.trace-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #667788;
text-align: center;
}
.empty-icon {
font-size: 24px;
margin-bottom: 8px;
opacity: 0.5;
}
.empty-text {
font-size: 11px;
margin-bottom: 4px;
font-family: 'JetBrains Mono', monospace;
}
.empty-hint {
font-size: 9px;
color: #445566;
font-family: 'JetBrains Mono', monospace;
}
/* Animation for new steps */
@keyframes trace-step-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.trace-step {
animation: trace-step-in 0.3s ease-out;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.reasoning-trace {
width: 280px;
}
.trace-content {
max-height: 200px;
}
}

View File

@@ -26,17 +26,11 @@ import threading
import hashlib
import os
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path
from datetime import datetime
from typing import Optional
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
"""Thread-per-request HTTP server."""
daemon_threads = True
# ── Configuration ──────────────────────────────────────────────────────
BRIDGE_PORT = int(os.environ.get('TIMMY_BRIDGE_PORT', 4004))
@@ -280,7 +274,7 @@ def main():
print(f" POST /bridge/move — Move user to room (user_id, room)")
print()
server = ThreadingHTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server = HTTPServer((BRIDGE_HOST, BRIDGE_PORT), BridgeHandler)
server.serve_forever()