agent-runtime/static/js/awareness.js
Nico 925fff731f v0.17.0: User expectation tracking, PA retry loop, machine state in PA context
- Memorizer tracks user_expectation (conversational/delegated/waiting_input/observing)
- Output node adjusts phrasing per expectation
- PA retry loop: reformulates job on expert failure (all retries exhausted or tool skip)
- Machine state in PA context: get_machine_summary includes current state, buttons, stored data
- Expert writes to machine state via update_machine + transition_machine
- Expanded baked schema coverage
- Awareness panel shows color-coded expectation state
- Dashboard and workspace component updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 19:03:07 +02:00

214 lines
7.2 KiB
JavaScript

/** Awareness panel: memorizer state, sensor readings.
* Node detail panel: per-node model, tokens, progress, last event.
*/
import { esc, truncate } from './util.js';
let _sensorReadings = {};
// --- Node state tracker ---
const _nodeState = {}; // { nodeName: { model, tokens, maxTokens, fillPct, lastEvent, lastDetail, status, toolCalls, startedAt } }
// Normalize node names to avoid duplicates (pa_v1→pa, expert_eras→eras, etc.)
function _normName(name) {
return name.replace('_v1', '').replace('_v2', '').replace('expert_', '');
}
function _getNode(name) {
const key = _normName(name);
if (!_nodeState[key]) {
_nodeState[key] = {
model: '', tokens: 0, maxTokens: 0, fillPct: 0,
lastEvent: '', lastDetail: '', status: 'idle',
toolCalls: 0, lastTool: '',
};
}
return _nodeState[key];
}
export function updateNodeFromHud(node, event, data) {
const n = _getNode(node);
if (event === 'context') {
if (data.model) n.model = data.model.replace('google/', '').replace('anthropic/', '');
if (data.tokens !== undefined) n.tokens = data.tokens;
if (data.max_tokens !== undefined) n.maxTokens = data.max_tokens;
if (data.fill_pct !== undefined) n.fillPct = data.fill_pct;
}
if (event === 'thinking') {
n.status = 'thinking';
n.lastEvent = 'thinking';
n.lastDetail = data.detail || '';
} else if (event === 'perceived') {
n.status = 'done';
n.lastEvent = 'perceived';
const a = data.analysis || {};
n.lastDetail = `${a.intent || '?'}/${a.language || '?'}/${a.tone || '?'}`;
} else if (event === 'decided' || event === 'routed') {
n.status = 'done';
n.lastEvent = event;
n.lastDetail = data.goal || data.instruction || data.job || '';
} else if (event === 'tool_call') {
n.status = 'tool';
n.lastEvent = 'tool_call';
n.lastTool = data.tool || '';
n.lastDetail = data.tool || '';
n.toolCalls++;
} else if (event === 'tool_result') {
n.lastEvent = 'tool_result';
n.lastDetail = truncate(data.output || '', 50);
} else if (event === 'streaming') {
n.status = 'streaming';
n.lastEvent = 'streaming';
} else if (event === 'done') {
n.status = 'done';
n.lastEvent = 'done';
} else if (event === 'updated') {
n.status = 'done';
n.lastEvent = 'updated';
} else if (event === 'planned') {
n.status = 'planned';
n.lastEvent = 'planned';
n.lastDetail = `${data.tools || 0} tools`;
} else if (event === 'interpreted') {
n.status = 'done';
n.lastEvent = 'interpreted';
n.lastDetail = truncate(data.summary || '', 50);
}
renderNodes();
}
// Fixed pipeline order — no re-sorting
// Fixed pipeline order using normalized names
const PIPELINE_ORDER = ['input', 'pa', 'director', 'eras', 'plankiste',
'thinker', 'interpreter', 'output', 'memorizer', 'ui', 'sensor'];
function renderNodes() {
const el = document.getElementById('node-metrics');
if (!el) return;
const entries = Object.entries(_nodeState)
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine');
const sorted = entries.sort((a, b) => {
const ia = PIPELINE_ORDER.indexOf(a[0]);
const ib = PIPELINE_ORDER.indexOf(b[0]);
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
});
let html = '';
for (const [name, n] of sorted) {
const statusClass = n.status === 'thinking' || n.status === 'tool' ? 'nm-active'
: n.status === 'streaming' ? 'nm-streaming' : '';
const shortName = name.replace('_v1', '').replace('_v2', '').replace('expert_', '');
const modelShort = n.model ? n.model.split('/').pop().replace('-001', '').replace('-4.5', '4.5') : '';
const tokenStr = n.maxTokens ? `${n.tokens}/${n.maxTokens}t` : '';
const fillW = n.fillPct || 0;
const detail = n.lastDetail ? truncate(n.lastDetail, 45) : '';
const toolStr = n.toolCalls > 0 ? ` [${n.toolCalls} calls]` : '';
html += `<div class="node-card ${statusClass}">
<div class="nc-header">
<span class="nc-name">${esc(shortName)}</span>
<span class="nc-model">${esc(modelShort)}</span>
<span class="nc-tokens">${esc(tokenStr)}</span>
</div>
<div class="nc-bar"><div class="nc-fill" style="width:${fillW}%"></div></div>
<div class="nc-status">
<span class="nc-event">${esc(n.lastEvent)}</span>
<span class="nc-detail">${esc(detail)}${esc(toolStr)}</span>
</div>
</div>`;
}
el.innerHTML = html;
}
export function initNodesFromGraph(graphData) {
// Populate node cards from graph definition (before any messages)
const nodes = graphData.nodes || {};
const details = graphData.node_details || {};
for (const [role, impl] of Object.entries(nodes)) {
const n = _getNode(role);
const d = details[role];
if (d) {
n.model = (d.model || '').replace('google/', '').replace('anthropic/', '');
n.maxTokens = d.max_tokens || 0;
}
n.lastEvent = 'idle';
n.status = 'idle';
}
renderNodes();
}
export function clearNodes() {
for (const key of Object.keys(_nodeState)) delete _nodeState[key];
const el = document.getElementById('node-metrics');
if (el) el.innerHTML = '';
}
// Keep old meter function for backward compat (called from ws.js)
export function updateMeter(node, tokens, maxTokens, fillPct) {
const n = _getNode(node);
n.tokens = tokens;
n.maxTokens = maxTokens;
n.fillPct = fillPct;
renderNodes();
}
// --- Awareness: memorizer state ---
export function updateAwarenessState(state) {
const body = document.getElementById('aw-state-body');
if (!body) return;
const expectation = state.user_expectation || 'conversational';
const expClass = {
conversational: 'aw-exp-conv',
delegated: 'aw-exp-deleg',
waiting_input: 'aw-exp-wait',
observing: 'aw-exp-obs',
}[expectation] || '';
const display = [
['user', state.user_name],
['mood', state.user_mood],
['expectation', expectation, expClass],
['topic', state.topic],
['lang', state.language],
['style', state.style_hint],
['situation', state.situation],
];
const facts = state.facts || [];
const history = state.topic_history || [];
let html = display.map(([k, v, cls]) =>
`<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val ${cls || ''}">${esc(v || 'null')}</span></div>`
).join('');
if (facts.length) {
html += '<div class="aw-row"><span class="aw-key">facts</span><span class="aw-val">'
+ facts.map(f => esc(truncate(f, 40))).join('<br>') + '</span></div>';
}
if (history.length) {
html += '<div class="aw-row"><span class="aw-key">topics</span><span class="aw-val">'
+ history.map(t => esc(truncate(t, 25))).join(', ') + '</span></div>';
}
body.innerHTML = html;
}
// --- Awareness: sensor readings ---
export function updateAwarenessSensors(tick, deltas) {
const body = document.getElementById('aw-sensor-body');
if (!body) return;
for (const [k, v] of Object.entries(deltas)) {
_sensorReadings[k] = v;
}
let html = `<div class="aw-row"><span class="aw-key">tick</span><span class="aw-val">#${tick}</span></div>`;
for (const [k, v] of Object.entries(_sensorReadings)) {
html += `<div class="aw-row"><span class="aw-key">${esc(k)}</span><span class="aw-val">${esc(String(v))}</span></div>`;
}
body.innerHTML = html;
}