mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
Watch the agent think. Watch memory change. Watch the receipt prove why.
Make Vestige the first memory server where you can replay an agent run,
audit every retrieval, and review changes to the agent's brain like code.
Phase 0 — the trace-correlation spine. One runId threads, unbroken, through
every layer: MCP tool output (runId + traceUri) -> SQLite agent_traces rows ->
WebSocket TraceEvent -> dashboard pulse -> /api/traces/:runId ->
vestige://trace/{runId} -> .vestige-trace.json export -> Cinema replay input.
Proven end to end by a real JSON-RPC round-trip integration test.
Core (vestige-core):
- trace/ module: MemoryTraceEvent (7 variants incl. contradiction.detected),
Receipt, and classify_write — the pure, DB-free immune-system logic.
- Risk taxonomy: contradiction-vs-high-trust, supersede/forget/merge/protect,
identity/preference/workflow/positioning, auth/security/money/legal,
dream consolidation, decay resurrection, low-confidence batch, weak-provenance
connector. Fast / Risk-Gated (default) / Paranoid modes.
- V18 migration: agent_traces, agent_runs, memory_receipts, memory_prs.
- trace_store.rs: CRUD following the established store idiom.
MCP (vestige-mcp):
- trace_recorder.rs: records mcp.call + downstream retrieve/suppress/write/
contradiction/veto/dream events; builds + persists receipts; risk-gates
writes into Memory PRs. Args are hashed, never stored raw.
- server.rs dispatch stamps runId/traceUri/receipt onto every tool result and
routes risky writes to the PR queue; trace events broadcast over WebSocket.
- vestige://trace/{runId} resource; /api/traces, /api/receipts, /api/memory-prs.
Dashboard:
- Black Box tab: live spine header + Proof Mode, run picker, timeline scrubber,
per-event detail, memory pulse, full event log, .vestige-trace.json export.
- Memory PRs tab: GitHub-style cognition diff, self-explaining risk signals,
Promote/Merge/Supersede/Quarantine/Forget/Ask-Agent-Why, mode toggle.
- ReceiptCard with "Open receipt in Cinema" (deep-links graph; Cinema untouched).
Gates: 987 lib tests pass, clippy -D warnings clean, dashboard check + build
clean. Live proof in blackbox-proof-2026-06-22/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
// ═══════════════════════════════════════════════════════════════════════════
|
|
// AGENT BLACK BOX — presentation helpers
|
|
// ───────────────────────────────────────────────────────────────────────────
|
|
// Pure functions that turn a raw `TraceEvent` into the label, color, glyph,
|
|
// and one-line summary the Black Box timeline renders. Kept out of the
|
|
// component so they are unit-testable and reused by the Proof Mode header.
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
import type { TraceEvent } from '$lib/stores/api';
|
|
|
|
export type TraceKind = TraceEvent['type'];
|
|
|
|
/** The accent color for each trace-event kind (CSS color value). */
|
|
export function eventColor(kind: TraceKind): string {
|
|
switch (kind) {
|
|
case 'mcp.call':
|
|
return 'var(--color-synapse-glow, #818cf8)';
|
|
case 'memory.retrieve':
|
|
return 'var(--color-recall, #10b981)';
|
|
case 'memory.suppress':
|
|
return '#a78bfa'; // violet — the forgetting hue
|
|
case 'memory.write':
|
|
return '#38bdf8'; // sky — a new write
|
|
case 'contradiction.detected':
|
|
return '#fb7185'; // rose — tension
|
|
case 'sanhedrin.veto':
|
|
return '#f43f5e'; // red — a block
|
|
case 'dream.patch':
|
|
return '#c084fc'; // purple — dream
|
|
default:
|
|
return 'var(--color-synapse, #6366f1)';
|
|
}
|
|
}
|
|
|
|
/** A short human label for each kind. */
|
|
export function eventLabel(kind: TraceKind): string {
|
|
switch (kind) {
|
|
case 'mcp.call':
|
|
return 'Tool Call';
|
|
case 'memory.retrieve':
|
|
return 'Retrieved';
|
|
case 'memory.suppress':
|
|
return 'Suppressed';
|
|
case 'memory.write':
|
|
return 'Wrote';
|
|
case 'contradiction.detected':
|
|
return 'Contradiction';
|
|
case 'sanhedrin.veto':
|
|
return 'Veto';
|
|
case 'dream.patch':
|
|
return 'Dream Patch';
|
|
default:
|
|
return kind;
|
|
}
|
|
}
|
|
|
|
/** A single glyph (emoji-free SVG path is overkill here; a compact symbol). */
|
|
export function eventGlyph(kind: TraceKind): string {
|
|
switch (kind) {
|
|
case 'mcp.call':
|
|
return '⟐';
|
|
case 'memory.retrieve':
|
|
return '◉';
|
|
case 'memory.suppress':
|
|
return '⊘';
|
|
case 'memory.write':
|
|
return '✎';
|
|
case 'contradiction.detected':
|
|
return '⚡';
|
|
case 'sanhedrin.veto':
|
|
return '⛔';
|
|
case 'dream.patch':
|
|
return '☾';
|
|
default:
|
|
return '•';
|
|
}
|
|
}
|
|
|
|
/** A one-line summary of what an event did, for the timeline row. */
|
|
export function eventSummary(ev: TraceEvent): string {
|
|
switch (ev.type) {
|
|
case 'mcp.call':
|
|
return `${ev.tool} · args ${ev.argsHash.slice(0, 8)}`;
|
|
case 'memory.retrieve':
|
|
return `${ev.ids.length} ${ev.ids.length === 1 ? 'memory' : 'memories'} surfaced`;
|
|
case 'memory.suppress':
|
|
return `${ev.id.slice(0, 8)} — ${ev.reason.replace('_', ' ')}`;
|
|
case 'memory.write':
|
|
return `${ev.id.slice(0, 8)} — ${ev.source}`;
|
|
case 'contradiction.detected':
|
|
return ev.detail;
|
|
case 'sanhedrin.veto':
|
|
return `"${ev.claim}" (conf ${(ev.confidence * 100).toFixed(0)}%)`;
|
|
case 'dream.patch':
|
|
return `${ev.proposalIds.length} consolidation proposal(s)`;
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/** The memory ids an event touched (for graph-pulse replay). */
|
|
export function eventMemoryIds(ev: TraceEvent): string[] {
|
|
switch (ev.type) {
|
|
case 'memory.retrieve':
|
|
return ev.ids;
|
|
case 'memory.suppress':
|
|
case 'memory.write':
|
|
return [ev.id];
|
|
case 'contradiction.detected':
|
|
return ev.ids;
|
|
case 'sanhedrin.veto':
|
|
return ev.evidenceIds;
|
|
case 'dream.patch':
|
|
return ev.proposalIds;
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Format a millisecond timestamp as a clock time. */
|
|
export function formatAt(at: number): string {
|
|
if (!Number.isFinite(at) || at <= 0) return '—';
|
|
const d = new Date(at);
|
|
return d.toLocaleTimeString(undefined, {
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
}
|
|
|
|
/** Elapsed milliseconds of an event relative to the run's first event. */
|
|
export function relativeMs(at: number, startAt: number): number {
|
|
return Math.max(0, at - startAt);
|
|
}
|