atlas-iac/dockerfiles/hermes-webui-hux/runtime/wave_a_activity_memory.js

277 lines
15 KiB
JavaScript

/* node:coverage disable */
(function (root, factory) {
'use strict';
const common = typeof module === 'object' && module.exports;
const foundation = common ? require('../foundation.js') : root.HermesHuxFoundation;
const shellApi = common ? require('../shell.js') : root.HermesHuxShell;
const contract = common ? require('./wave_a_contract.js') : root.HermesHuxWaveAContract;
const api = factory(foundation, shellApi, contract);
if (common) module.exports = api;
else root.HermesHuxWaveA = api;
}(typeof globalThis === 'object' ? globalThis : this, function (foundation, shellApi, contract) {
/* node:coverage enable */
'use strict';
const KIND_LABELS = Object.freeze({
'message.user': 'You replied', 'message.assistant': 'Hermes replied',
'decision.route': 'Route selected', 'decision.plan': 'Plan updated',
'tool.call': 'Tool started', 'tool.result': 'Tool finished',
'approval.requested': 'Approval requested', 'approval.resolved': 'Approval resolved',
'memory.proposed': 'Memory suggested', 'memory.committed': 'Memory saved',
'memory.forgotten': 'Memory forgotten', 'memory.suppressed': 'Memory not saved',
'memory.retrieval_removed': 'Memory retrieval changed', 'artifact.created': 'Artifact created',
'artifact.version': 'Artifact updated', 'artifact.promoted': 'Artifact promoted',
'citation.attached': 'Citation attached', 'mode.changed': 'Mode changed',
'run.started': 'Run started', 'run.cancelled': 'Run cancelled',
'run.completed': 'Run completed', 'run.failed': 'Run failed',
'privacy.notice': 'Privacy notice', 'suggestion.shown': 'Suggestion shown',
'suggestion.dismissed': 'Suggestion dismissed', 'release.transition': 'Release advanced',
'delegation.started': 'Delegation started', 'delegation.completed': 'Delegation completed',
'delegation.failed': 'Delegation failed', 'budget.exhausted': 'Budget exhausted',
'side_effect.blocked': 'Action blocked', 'side_effect.released': 'Action approved',
});
function element(doc, tag, attributes, text) {
const node = doc.createElement(tag);
Object.entries(attributes || {}).forEach(([name, value]) => node.setAttribute(name, value));
if (text !== undefined && text !== null) node.textContent = String(text);
return node;
}
function announce(doc, container, message, alert) {
const box = element(doc, 'p', {'class': alert ? 'hux-wave-a__error' : 'hux-wave-a__status',
'role': alert ? 'alert' : 'status', 'aria-live': alert ? 'assertive' : 'polite'}, message);
container.replaceChildren(box);
return box;
}
function friendlyKind(kind) {
return KIND_LABELS[kind] || String(kind).replace(/[._]/g, ' ');
}
function activityExtension(options) {
const doc = options.document;
const conversationId = options.conversationId;
if (!contract.isId(conversationId, 'conv')) throw new TypeError('Activity requires an opaque conversation id');
return Object.freeze({id: 'wave-a-activity', flag: 'hux.activity_timeline', label: 'Activity', order: 10,
render(context) {
const panel = context.container;
let generation = 0;
function show(page) {
const section = element(doc, 'section', {'class': 'hux-wave-a hux-wave-a--activity',
'aria-labelledby': 'hux-wave-a-activity-title'});
const heading = element(doc, 'header', {'class': 'hux-wave-a__heading'});
heading.appendChild(element(doc, 'div', {}, null));
heading.children[0].appendChild(element(doc, 'h3', {'id': 'hux-wave-a-activity-title'}, 'What is happening'));
heading.children[0].appendChild(element(doc, 'p', {}, 'A compact, privacy-filtered timeline for this conversation.'));
const refresh = element(doc, 'button', {'type': 'button'}, 'Refresh activity');
refresh.addEventListener('click', load);
heading.appendChild(refresh);
section.appendChild(heading);
const list = element(doc, 'ol', {'class': 'hux-wave-a__timeline'});
page.items.forEach((item) => {
const row = element(doc, 'li', {'class': 'hux-wave-a__event', 'data-kind': item.kind});
const stamp = element(doc, 'time', {'datetime': item.timestamp}, new Date(item.timestamp).toLocaleString());
const title = element(doc, 'strong', {}, friendlyKind(item.kind));
const summary = element(doc, 'p', {}, item.summary);
row.appendChild(stamp); row.appendChild(title); row.appendChild(summary);
if (item.evidence.length) {
const details = element(doc, 'details', {'class': 'hux-wave-a__evidence'});
details.appendChild(element(doc, 'summary', {}, `${item.evidence.length} evidence reference${item.evidence.length === 1 ? '' : 's'}`));
const refs = element(doc, 'ul');
item.evidence.forEach((ref) => refs.appendChild(element(doc, 'li', {}, `${ref.kind}: ${ref.id}`)));
details.appendChild(refs); row.appendChild(details);
}
list.appendChild(row);
});
section.appendChild(list);
if (!page.items.length) section.appendChild(element(doc, 'p', {'class': 'hux-wave-a__empty'}, 'No activity has been recorded yet.'));
section.appendChild(element(doc, 'p', {'class': 'hux-wave-a__sr', 'role': 'status',
'aria-live': 'polite'}, `${page.items.length} activity events available`));
panel.replaceChildren(section);
}
async function load() {
const current = ++generation;
announce(doc, panel, 'Loading activity…', false);
try {
const response = await context.client.request(`/conversations/${conversationId}/events?after_seq=0&limit=200`);
if (current !== generation) return;
show(contract.normalizeEventPage(response.body, context.client.identity, conversationId));
} catch (_) {
if (current === generation) announce(doc, panel,
'Activity could not be verified. No event details were shown.', true);
}
}
load();
}});
}
function memoryActions(item) {
if (item.status === 'proposed') return [['approve', 'Approve'], ['reject', 'Reject']];
if (item.status === 'active') return [
[item.retrievable ? 'remove_retrieval' : 'restore_retrieval',
item.retrievable ? 'Stop using in replies' : 'Use in replies'], ['forget', 'Forget'],
];
if (item.status === 'expired') return [['forget', 'Forget']];
return [];
}
function memoryExtension(options) {
const doc = options.document;
const conversationId = options.conversationId;
const keyFactory = options.idempotencyKey;
if (!contract.isId(conversationId, 'conv')) throw new TypeError('Memory requires an opaque conversation id');
if (typeof keyFactory !== 'function') throw new TypeError('Memory requires an idempotency key factory');
return Object.freeze({id: 'wave-a-memory', flag: 'hux.memory_control', label: 'Memory', order: 20,
render(context) {
const panel = context.container;
let generation = 0;
async function mutate(item, action, body) {
announce(doc, panel, `${friendlyKind(`memory.${action}`)}`, false);
try {
await context.client.request(`/memory/${encodeURIComponent(item.id)}/${action}`, {
method: 'POST', headers: {'If-Match': String(item.revision)}, ...(body ? {body} : {}),
});
await load();
} catch (error) {
announce(doc, panel, error && error.status === 409 ?
'This memory changed. Refresh before trying again.' : 'The memory change could not be completed.', true);
}
}
function card(item) {
const article = element(doc, 'article', {'class': 'hux-wave-a__memory', 'data-status': item.status});
const header = element(doc, 'header');
header.appendChild(element(doc, 'span', {'class': 'hux-wave-a__pill'}, item.kind));
header.appendChild(element(doc, 'strong', {}, item.status));
article.appendChild(header);
article.appendChild(element(doc, 'p', {'class': 'hux-wave-a__memory-content'}, item.content));
const why = element(doc, 'dl', {'class': 'hux-wave-a__meta'});
[['Why saved', item.reason], ['Scope', item.scope.scopeId ? `${item.scope.level}: ${item.scope.scopeId}` : item.scope.level],
['Privacy', item.sensitivity], ['Retention', item.retention]].forEach(([label, value]) => {
const row = element(doc, 'div'); row.appendChild(element(doc, 'dt', {}, label));
row.appendChild(element(doc, 'dd', {}, value)); why.appendChild(row);
});
article.appendChild(why);
const actions = element(doc, 'div', {'class': 'hux-wave-a__actions'});
memoryActions(item).forEach(([action, label]) => {
const button = element(doc, 'button', {'type': 'button'}, label);
button.addEventListener('click', () => mutate(item, action)); actions.appendChild(button);
});
article.appendChild(actions);
if (item.status === 'active') {
const edit = element(doc, 'form', {'class': 'hux-wave-a__edit'});
const fieldId = `hux-memory-edit-${item.id}`;
edit.appendChild(element(doc, 'label', {'for': fieldId}, 'Correct this memory'));
const input = element(doc, 'textarea', {'id': fieldId, 'maxlength': '2000', 'required': 'required'});
edit.appendChild(input);
const submit = element(doc, 'button', {'type': 'submit'}, 'Save as a corrected memory');
edit.appendChild(submit);
edit.addEventListener('submit', (event) => {
event.preventDefault();
const content = contract.safeText(input.value, '', 2000);
if (content) mutate(item, 'edit', {content});
});
article.appendChild(edit);
}
return article;
}
function show(page) {
const section = element(doc, 'section', {'class': 'hux-wave-a hux-wave-a--memory',
'aria-labelledby': 'hux-wave-a-memory-title'});
const heading = element(doc, 'header', {'class': 'hux-wave-a__heading'});
const title = element(doc, 'div');
title.appendChild(element(doc, 'h3', {'id': 'hux-wave-a-memory-title'}, 'Memory control'));
title.appendChild(element(doc, 'p', {}, 'See what Hermes remembers, why, and how it is used.'));
heading.appendChild(title);
const refresh = element(doc, 'button', {'type': 'button'}, 'Refresh memory');
refresh.addEventListener('click', load); heading.appendChild(refresh); section.appendChild(heading);
const propose = element(doc, 'form', {'class': 'hux-wave-a__propose'});
propose.appendChild(element(doc, 'h4', {}, 'Suggest a memory'));
const content = element(doc, 'textarea', {'maxlength': '2000', 'required': 'required',
'aria-label': 'What should Hermes remember?'});
const reason = element(doc, 'input', {'maxlength': '280', 'required': 'required',
'aria-label': 'Why should Hermes remember this?'});
const kind = element(doc, 'select', {'aria-label': 'Memory kind'});
['preference', 'fact', 'instruction', 'context'].forEach((value) =>
kind.appendChild(element(doc, 'option', {'value': value}, value)));
propose.appendChild(content); propose.appendChild(reason); propose.appendChild(kind);
propose.appendChild(element(doc, 'button', {'type': 'submit'}, 'Propose for review'));
propose.addEventListener('submit', async (event) => {
event.preventDefault();
const clean = contract.safeText(content.value, '', 2000);
const why = contract.safeText(reason.value, '', 280);
if (!clean || !why) return;
announce(doc, panel, 'Submitting memory proposal…', false);
try {
const idempotencyKey = contract.safeIdempotencyKey(keyFactory());
await context.client.request('/memory', {method: 'POST',
headers: {'Idempotency-Key': idempotencyKey}, body: {kind: kind.value || 'fact', content: clean,
reason: why, approval_mode: 'ask', conversation_id: conversationId,
scope: {level: 'conversation', scope_id: conversationId}}});
await load();
} catch (_) {
announce(doc, panel, 'The memory proposal could not be completed.', true);
}
});
section.appendChild(propose);
const list = element(doc, 'div', {'class': 'hux-wave-a__memory-list'});
page.items.forEach((item) => list.appendChild(card(item)));
section.appendChild(list);
if (!page.items.length) section.appendChild(element(doc, 'p', {'class': 'hux-wave-a__empty'}, 'No memory entries are available.'));
section.appendChild(element(doc, 'p', {'class': 'hux-wave-a__sr', 'role': 'status',
'aria-live': 'polite'}, `${page.items.length} memory entries available`));
panel.replaceChildren(section);
}
async function load() {
const current = ++generation;
announce(doc, panel, 'Loading memory…', false);
try {
const response = await context.client.request('/memory');
if (current !== generation) return;
show(contract.normalizeMemoryPage(response.body, context.client.identity, conversationId));
} catch (_) {
if (current === generation) announce(doc, panel,
'Memory could not be verified. No saved details were shown.', true);
}
}
load();
}});
}
function defaultKeyFactory() {
let sequence = 0;
return () => `webui:memory:${Date.now().toString(36)}:${(++sequence).toString(36)}`;
}
function createWaveARuntime(options) {
const settings = options || {};
const doc = settings.document || (typeof document === 'object' ? document : null);
if (!doc) throw new TypeError('Wave A runtime requires a document');
const client = contract.createCanonicalClient(settings);
const shell = shellApi.createShell({client, document: doc, instanceId: settings.instanceId || 'wave-a'});
const shared = {document: doc, conversationId: settings.conversationId,
idempotencyKey: settings.idempotencyKey || defaultKeyFactory()};
const removeActivity = shell.register(activityExtension(shared));
const removeMemory = shell.register(memoryExtension(shared));
let mounted = false;
return Object.freeze({client,
async mount(target) {
if (mounted) throw new Error('Wave A runtime is already mounted');
mounted = true; shell.mount(target); await client.negotiate();
},
destroy() {
removeActivity(); removeMemory(); shell.destroy(); mounted = false;
},
});
}
return Object.freeze({activityExtension, createWaveARuntime, friendlyKind, memoryActions, memoryExtension});
}));