Loading...
Loading...
Hermes Labyrinth observability plugin for monitoring autonomous agent journeys, crossings, and execution traces
npx skill4agent add aradotso/hermes-skills hermes-labyrinth-observabilitySkill by ara.so — Hermes Skills collection.
mkdir -p ~/.hermes/plugins
git clone https://github.com/stainlu/hermes-labyrinth.git ~/.hermes/plugins/hermes-labyrinthhermes dashboardhttp://127.0.0.1:9119mkdir -p ~/.hermes/plugins
git clone https://github.com/stainlu/hermes-labyrinth.git ~/.hermes/plugins/hermes-labyrinth
cd ~/.hermes/plugins/hermes-labyrinth
git checkout v0.1.3 # Pin to reviewed versioncurl http://127.0.0.1:9119/api/dashboard/plugins/rescanhermes plugins disable hermes-labyrinth
rm -rf ~/.hermes/plugins/hermes-labyrinth
curl http://127.0.0.1:9119/api/dashboard/plugins/rescancurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/healthcurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeyscurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys/{journey_id}curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys/{journey_id}/crossingscurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/skillsskillsshadowedduplicateserrorscurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/croncurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/guidepostscurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/{journey_id}.json > journey.jsoncurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/{journey_id}.md > journey.mddashboard/plugin_api.pyfrom fastapi import APIRouter, HTTPException
from typing import Dict, Any
import os
router = APIRouter(prefix="/api/plugins/hermes-labyrinth")
@router.get("/custom-endpoint")
async def custom_endpoint() -> Dict[str, Any]:
"""Example custom endpoint for plugin."""
hermes_home = os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes"))
# Read-only access to Hermes state
state_db = os.path.join(hermes_home, "state.db")
if not os.path.exists(state_db):
raise HTTPException(status_code=503, detail="Hermes state unavailable")
return {
"status": "ok",
"hermes_home": hermes_home
}~/.hermes/state.db~/.hermes/skills/~/.hermes/cron/import sqlite3
import os
def query_journeys(hermes_home: str):
"""Query recent sessions from Hermes state.db"""
state_db = os.path.join(hermes_home, "state.db")
conn = sqlite3.connect(state_db)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT session_id, created_at, updated_at, status, model
FROM sessions
ORDER BY updated_at DESC
LIMIT 100
""")
sessions = [dict(row) for row in cursor.fetchall()]
conn.close()
return sessionsnpm run builddashboard/dist/labyrinth-bundle.jssrc/parts/*.jsdashboard/dist/labyrinth.csssrc/labyrinth.cssindex.html# Build and verify
npm run build
npm run check
# Run smoke tests
npm run smoke
# Test live demo
npm run smoke:live// src/parts/01-state.js - Global state management
const LabyrinthState = {
journeys: [],
selectedJourney: null,
crossings: [],
mode: 'chronological'
};
// src/parts/02-api.js - API client
async function fetchJourneys() {
const response = await fetch('/api/plugins/hermes-labyrinth/journeys');
return response.json();
}
// src/parts/03-components.js - UI components
function renderJourneyCard(journey) {
return `
<div class="journey-card" data-id="${journey.id}">
<h3>${escapeHtml(journey.title)}</h3>
<time>${new Date(journey.created_at * 1000).toLocaleString()}</time>
</div>
`;
}mkdir -p ~/.hermes/dashboard-themes
cp ~/.hermes/plugins/hermes-labyrinth/theme/hermes-labyrinth.yaml ~/.hermes/dashboard-themes/dashboard/manifest.json{
"name": "hermes-labyrinth",
"version": "0.1.3",
"title": "Labyrinth",
"description": "Agent journey observability",
"entry_js": "dist/labyrinth-bundle.js",
"entry_css": "dist/labyrinth.css",
"api_module": "plugin_api"
}# Get all journeys and filter with jq
curl -s http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys | \
jq '.journeys[] | select(.status == "completed")'# Get crossings and count tool types
curl -s http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys/{journey_id}/crossings | \
jq '.crossings[] | select(.type == "tool_call") | .tool_name' | \
sort | uniq -c# Export last 10 journeys as JSON
curl -s http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys | \
jq -r '.journeys[].id' | head -10 | while read id; do
curl -s "http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/$id.json" > "journey_$id.json"
done# List failed journeys with error details
curl -s http://127.0.0.1:9119/api/plugins/hermes-labyrinth/journeys | \
jq '.journeys[] | select(.status == "failed") | {id, error, updated_at}'from hermes.redactor import redact_secrets
def safe_export(content: str) -> str:
"""Apply redaction before export."""
try:
return redact_secrets(content)
except Exception:
# Fail closed if redactor unavailable
return "[redaction unavailable]"# Create test journey with dummy API keys in prompts/outputs
hermes chat "Test: sk-dummy-key-12345"
# Verify redaction in UI and exports
curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/{journey_id}.json | grep "sk-dummy"
# Should return nothing if redaction works
curl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/reports/{journey_id}.md | grep "sk-dummy"
# Should return nothing if redaction worksnpm testpython3 scripts/test-plugin-api.pynpm run smokels -la ~/.hermes/plugins/hermes-labyrinth/dashboard/manifest.json# Stop dashboard
hermes dashboard stop
# Start dashboard
hermes dashboardpython3 -c "import sys; sys.path.insert(0, '$HOME/.hermes/plugins/hermes-labyrinth/dashboard'); import plugin_api"ls -la ~/.hermes/state.db[redaction unavailable]from hermes.redactor import redact_secretssqlite3 ~/.hermes/state.db "SELECT COUNT(*) FROM sessions;"ls -la ~/.hermes/state.dbcurl http://127.0.0.1:9119/api/plugins/hermes-labyrinth/health# Clean and rebuild
rm -rf dashboard/dist index.html
npm run build
npm run checkdocs/CONCEPT.mddocs/DESIGN_BRIEF.mddocs/FUNCTIONAL_SPEC.md