- 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>
214 lines
7.2 KiB
JavaScript
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;
|
|
}
|