Files
hermes-agent/session_viewer.html
teknium1 783acd712d feat: implement code execution sandbox for programmatic tool calling
- Introduced a new `execute_code` tool that allows the agent to run Python scripts that call Hermes tools via RPC, reducing the number of round trips required for tool interactions.
- Added configuration options for timeout and maximum tool calls in the sandbox environment.
- Updated the toolset definitions to include the new code execution capabilities, ensuring integration across platforms.
- Implemented comprehensive tests for the code execution sandbox, covering various scenarios including tool call limits and error handling.
- Enhanced the CLI and documentation to reflect the new functionality, providing users with clear guidance on using the code execution tool.
2026-02-19 23:23:43 -08:00

683 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermes Agent - Session Viewer</title>
<style>
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2: #1c2333;
--border: #30363d;
--text: #e6edf3;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent-dim: #1f3a5f;
--user: #da8ee7;
--user-bg: #2d1b3d;
--assistant: #58a6ff;
--assistant-bg: #152238;
--tool: #3fb950;
--tool-bg: #12261e;
--system: #d29922;
--system-bg: #2a2000;
--error: #f85149;
--meta: #768390;
--radius: 10px;
--font-mono: 'SF Mono', 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.6;
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
#sidebar {
width: 340px;
min-width: 340px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
#sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#sidebar-header h1 {
font-size: 18px;
font-weight: 700;
color: var(--accent);
margin-bottom: 4px;
letter-spacing: -0.3px;
}
#sidebar-header p {
color: var(--text-muted);
font-size: 12px;
}
#file-picker {
padding: 12px 20px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
#file-picker label {
display: block;
padding: 10px 16px;
background: var(--accent-dim);
border: 1px dashed var(--accent);
border-radius: var(--radius);
text-align: center;
cursor: pointer;
color: var(--accent);
font-size: 13px;
font-weight: 500;
transition: all 0.15s;
}
#file-picker label:hover {
background: #1a4478;
}
#file-picker input { display: none; }
#session-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.session-item {
padding: 12px 14px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
margin-bottom: 2px;
}
.session-item:hover { background: var(--surface2); }
.session-item.active { background: var(--accent-dim); border: 1px solid var(--accent); }
.session-item .session-title {
font-weight: 600;
font-size: 13px;
color: var(--text);
margin-bottom: 3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-item .session-meta {
display: flex;
gap: 10px;
font-size: 11px;
color: var(--text-muted);
}
.session-item .session-meta .badge {
display: inline-block;
padding: 1px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
}
.badge-cli { background: #1a3a2a; color: #3fb950; }
.badge-telegram { background: #1a2a3a; color: #58a6ff; }
.badge-discord { background: #2a1a3a; color: #bc8cff; }
/* Main area */
#main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#session-header {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
}
#session-header h2 {
font-size: 16px;
font-weight: 600;
margin-bottom: 6px;
}
#session-header .meta-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
font-size: 12px;
color: var(--text-muted);
}
#session-header .meta-row span { display: flex; align-items: center; gap: 4px; }
#messages-container {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
/* Welcome state */
#welcome {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
gap: 12px;
}
#welcome .icon { font-size: 48px; opacity: 0.3; }
#welcome h3 { font-size: 18px; color: var(--text); font-weight: 600; }
/* Messages */
.message {
margin-bottom: 16px;
border-radius: var(--radius);
overflow: hidden;
border: 1px solid var(--border);
}
.message-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.message-body {
padding: 12px 16px;
white-space: pre-wrap;
word-wrap: break-word;
font-size: 13.5px;
line-height: 1.65;
}
.msg-user .message-header { background: var(--user-bg); color: var(--user); }
.msg-user .message-body { background: #1e1228; }
.msg-user { border-color: #3d2650; }
.msg-assistant .message-header { background: var(--assistant-bg); color: var(--assistant); }
.msg-assistant .message-body { background: #0f1a2e; }
.msg-assistant { border-color: #1e3a5f; }
.msg-tool .message-header { background: var(--tool-bg); color: var(--tool); }
.msg-tool .message-body { background: #0c1a14; font-family: var(--font-mono); font-size: 12px; }
.msg-tool { border-color: #1a3525; }
.msg-session_meta .message-header { background: var(--system-bg); color: var(--system); }
.msg-session_meta .message-body { background: #1a1800; }
.msg-session_meta { border-color: #3a3000; }
.msg-system .message-header { background: var(--system-bg); color: var(--system); }
.msg-system .message-body { background: #1a1800; }
.msg-system { border-color: #3a3000; }
.tool-calls-section {
margin-top: 8px;
border-top: 1px solid var(--border);
padding-top: 8px;
}
.tool-call-item {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 6px;
overflow: hidden;
}
.tool-call-name {
padding: 6px 10px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
color: var(--tool);
background: var(--tool-bg);
border-bottom: 1px solid var(--border);
}
.tool-call-args {
padding: 8px 10px;
font-family: var(--font-mono);
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
color: var(--text-muted);
max-height: 300px;
overflow-y: auto;
}
/* System prompt collapsible */
.system-prompt-toggle {
padding: 10px 16px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 16px;
cursor: pointer;
user-select: none;
}
.system-prompt-toggle summary {
font-size: 12px;
font-weight: 600;
color: var(--system);
text-transform: uppercase;
letter-spacing: 0.5px;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
}
.system-prompt-toggle summary::before {
content: '\25B6';
font-size: 10px;
transition: transform 0.15s;
}
.system-prompt-toggle[open] summary::before {
transform: rotate(90deg);
}
.system-prompt-content {
margin-top: 10px;
padding: 12px;
background: var(--bg);
border-radius: 6px;
font-size: 12px;
white-space: pre-wrap;
word-wrap: break-word;
color: var(--text-muted);
max-height: 400px;
overflow-y: auto;
line-height: 1.5;
}
.timestamp {
font-size: 11px;
color: var(--meta);
font-family: var(--font-mono);
}
.tool-result-truncated {
max-height: 400px;
overflow-y: auto;
}
/* Scrollbar styling */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #484f58; }
.no-content { color: var(--text-muted); font-style: italic; font-size: 12px; }
.reasoning-block {
margin-top: 8px;
padding: 8px 12px;
background: #1a1a2e;
border: 1px solid #2a2a4e;
border-radius: 6px;
font-size: 12px;
color: #a0a0d0;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.reasoning-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #7070b0;
margin-bottom: 4px;
}
.session-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0;
color: var(--text-muted);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1px;
}
.session-divider::before, .session-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.stats-bar {
display: flex;
gap: 16px;
padding: 8px 14px;
background: var(--surface2);
border-radius: 6px;
margin-bottom: 16px;
font-size: 12px;
}
.stats-bar .stat { display: flex; align-items: center; gap: 4px; }
.stats-bar .stat-label { color: var(--text-muted); }
.stats-bar .stat-value { color: var(--text); font-weight: 600; font-family: var(--font-mono); }
</style>
</head>
<body>
<div id="sidebar">
<div id="sidebar-header">
<h1>Hermes Agent</h1>
<p>Session Transcript Viewer</p>
</div>
<div id="file-picker">
<label for="jsonl-input">Load .jsonl file</label>
<input type="file" id="jsonl-input" accept=".jsonl,.json,.txt">
</div>
<div id="session-list"></div>
</div>
<div id="main">
<div id="session-header" style="display:none"></div>
<div id="messages-container">
<div id="welcome">
<div class="icon">&#x2699;</div>
<h3>Load a session file</h3>
<p>Select a .jsonl file from the sidebar to view exported Hermes Agent sessions.</p>
</div>
</div>
</div>
<script>
const sessions = [];
let activeIdx = -1;
document.getElementById('jsonl-input').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
sessions.length = 0;
const lines = ev.target.result.split('\n').filter(l => l.trim());
for (const line of lines) {
try { sessions.push(JSON.parse(line)); } catch {}
}
renderSessionList();
if (sessions.length > 0) selectSession(0);
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded from ${file.name}`;
};
reader.readAsText(file);
});
function renderSessionList() {
const list = document.getElementById('session-list');
list.innerHTML = '';
sessions.forEach((s, i) => {
const firstUserMsg = (s.messages || []).find(m => m.role === 'user');
const preview = firstUserMsg
? firstUserMsg.content.substring(0, 80).replace(/\n/g, ' ')
: '(no messages)';
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
const dateStr = dt ? dt.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
const div = document.createElement('div');
div.className = 'session-item' + (i === activeIdx ? ' active' : '');
div.onclick = () => selectSession(i);
div.innerHTML = `
<div class="session-title">${esc(preview)}</div>
<div class="session-meta">
<span class="badge badge-${s.source || 'cli'}">${s.source || 'cli'}</span>
<span>${dateStr}</span>
<span>${s.message_count || 0} msgs</span>
</div>
`;
list.appendChild(div);
});
}
function selectSession(idx) {
activeIdx = idx;
const s = sessions[idx];
document.querySelectorAll('.session-item').forEach((el, i) => {
el.classList.toggle('active', i === idx);
});
const header = document.getElementById('session-header');
header.style.display = 'block';
const dt = s.started_at ? new Date(s.started_at * 1000) : null;
const endDt = s.ended_at ? new Date(s.ended_at * 1000) : null;
const duration = s.started_at && s.ended_at
? formatDuration(s.ended_at - s.started_at)
: 'unknown';
header.innerHTML = `
<h2>Session ${esc(s.id)}</h2>
<div class="meta-row">
<span>&#x1F4E1; ${esc(s.source || 'cli')}</span>
<span>&#x1F916; ${esc(s.model || 'unknown')}</span>
<span>&#x1F4AC; ${s.message_count || 0} messages</span>
<span>&#x1F527; ${s.tool_call_count || 0} tool calls</span>
<span>&#x23F1; ${duration}</span>
${s.end_reason ? `<span>&#x1F3C1; ${esc(s.end_reason)}</span>` : ''}
${dt ? `<span>&#x1F4C5; ${dt.toLocaleString()}</span>` : ''}
</div>
`;
renderMessages(s);
}
function renderMessages(session) {
const container = document.getElementById('messages-container');
container.innerHTML = '';
// System prompt (collapsible)
if (session.system_prompt) {
const details = document.createElement('details');
details.className = 'system-prompt-toggle';
details.innerHTML = `
<summary>System Prompt (${(session.system_prompt.length / 1024).toFixed(1)}KB)</summary>
<div class="system-prompt-content">${esc(session.system_prompt)}</div>
`;
container.appendChild(details);
}
// Stats bar
const stats = document.createElement('div');
stats.className = 'stats-bar';
stats.innerHTML = `
<div class="stat"><span class="stat-label">Messages:</span><span class="stat-value">${session.message_count || 0}</span></div>
<div class="stat"><span class="stat-label">Tool Calls:</span><span class="stat-value">${session.tool_call_count || 0}</span></div>
<div class="stat"><span class="stat-label">Source:</span><span class="stat-value">${esc(session.source || 'cli')}</span></div>
${session.user_id ? `<div class="stat"><span class="stat-label">User ID:</span><span class="stat-value">${esc(session.user_id)}</span></div>` : ''}
`;
container.appendChild(stats);
const messages = session.messages || [];
for (const msg of messages) {
const el = renderMessage(msg);
container.appendChild(el);
}
container.scrollTop = 0;
}
function renderMessage(msg) {
const div = document.createElement('div');
const role = msg.role || 'unknown';
div.className = `message msg-${role}`;
const roleIcon = {
user: '&#x1F464;',
assistant: '&#x1F916;',
tool: '&#x1F527;',
session_meta: '&#x2699;',
system: '&#x1F4CB;'
}[role] || '&#x2753;';
const ts = msg.timestamp ? new Date(msg.timestamp * 1000).toLocaleTimeString() : '';
const toolName = msg.tool_name ? ` (${msg.tool_name})` : '';
let headerExtra = '';
if (msg.tool_call_id && role === 'tool') {
headerExtra = ` &mdash; <span style="opacity:0.7;font-size:10px;text-transform:none;letter-spacing:0">${esc(msg.tool_call_id.substring(0, 24))}...</span>`;
}
div.innerHTML = `<div class="message-header">
<span>${roleIcon}</span>
<span>${role}${toolName}</span>
${headerExtra}
<span style="margin-left:auto" class="timestamp">${ts}</span>
</div>`;
const body = document.createElement('div');
body.className = 'message-body';
// Content
if (msg.content) {
let text = msg.content;
// Try to detect if content is a JSON string and pretty-print it
if (role === 'tool' && text.startsWith('{')) {
try {
const parsed = JSON.parse(text);
text = JSON.stringify(parsed, null, 2);
} catch {}
}
const contentDiv = document.createElement('div');
if (role === 'tool') {
contentDiv.className = 'tool-result-truncated';
}
contentDiv.textContent = text;
body.appendChild(contentDiv);
} else if (role !== 'session_meta' && !msg.tool_calls) {
const empty = document.createElement('span');
empty.className = 'no-content';
empty.textContent = '(no text content)';
body.appendChild(empty);
}
// Reasoning
if (msg.reasoning) {
const rBlock = document.createElement('div');
rBlock.innerHTML = `<div class="reasoning-label">Reasoning</div>`;
const rContent = document.createElement('div');
rContent.className = 'reasoning-block';
rContent.textContent = msg.reasoning;
rBlock.appendChild(rContent);
body.appendChild(rBlock);
}
// Tool calls
if (msg.tool_calls && msg.tool_calls.length > 0) {
const tcSection = document.createElement('div');
tcSection.className = 'tool-calls-section';
const label = document.createElement('div');
label.style.cssText = 'font-size:11px;font-weight:700;color:var(--tool);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;';
label.textContent = `Tool Calls (${msg.tool_calls.length})`;
tcSection.appendChild(label);
for (const tc of msg.tool_calls) {
const fn = tc.function || {};
const tcItem = document.createElement('div');
tcItem.className = 'tool-call-item';
const nameDiv = document.createElement('div');
nameDiv.className = 'tool-call-name';
nameDiv.textContent = fn.name || 'unknown';
tcItem.appendChild(nameDiv);
if (fn.arguments) {
const argsDiv = document.createElement('div');
argsDiv.className = 'tool-call-args';
let argsText = fn.arguments;
try {
argsText = JSON.stringify(JSON.parse(fn.arguments), null, 2);
} catch {}
argsDiv.textContent = argsText;
tcItem.appendChild(argsDiv);
}
tcSection.appendChild(tcItem);
}
body.appendChild(tcSection);
}
div.appendChild(body);
return div;
}
function esc(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function formatDuration(seconds) {
if (seconds < 60) return `${Math.round(seconds)}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h}h ${m}m`;
}
// Auto-load if file is in same directory (for local dev)
window.addEventListener('DOMContentLoaded', () => {
fetch('exprted.jsonl')
.then(r => { if (!r.ok) throw new Error(); return r.text(); })
.then(text => {
const lines = text.split('\n').filter(l => l.trim());
for (const line of lines) {
try { sessions.push(JSON.parse(line)); } catch {}
}
if (sessions.length) {
renderSessionList();
selectSession(sessions.length - 1);
document.querySelector('#sidebar-header p').textContent = `${sessions.length} sessions loaded`;
}
})
.catch(() => {});
});
</script>
</body>
</html>