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
4 changed files with 715 additions and 0 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

@@ -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;
}
}