v0.15.7: Fix action routing for v4, WS error handling, stable nodes panel
Action routing: - Button clicks now route through PA→Expert in v4 (was missing has_pa check) - Previously crashed with KeyError on missing thinker node WS error handling: - Exceptions in WS handler caught and logged, not crash - Frontend receives error HUD event instead of disconnect - Prevents 1006 reconnect loops on action errors Nodes panel: - Fixed pipeline order (no re-sorting on events) - Deduplicated node names (pa_v1→pa, expert_eras→eras) - Normalized names in state tracker Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84fa0830d8
commit
faeb9d3254
20
agent/api.py
20
agent/api.py
@ -153,12 +153,20 @@ def register_routes(app):
|
||||
msg = json.loads(data)
|
||||
# Always use current runtime (may change after graph switch)
|
||||
rt = _active_runtime or runtime
|
||||
if msg.get("type") == "action":
|
||||
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
|
||||
elif msg.get("type") == "cancel_process":
|
||||
rt.process_manager.cancel(msg.get("pid", 0))
|
||||
else:
|
||||
await rt.handle_message(msg.get("text", ""), dashboard=msg.get("dashboard"))
|
||||
try:
|
||||
if msg.get("type") == "action":
|
||||
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
|
||||
elif msg.get("type") == "cancel_process":
|
||||
rt.process_manager.cancel(msg.get("pid", 0))
|
||||
else:
|
||||
await rt.handle_message(msg.get("text", ""), dashboard=msg.get("dashboard"))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
log.error(f"[ws] handler error: {e}\n{traceback.format_exc()}")
|
||||
try:
|
||||
await ws.send_text(json.dumps({"type": "hud", "node": "runtime", "event": "error", "detail": str(e)[:200]}))
|
||||
except Exception:
|
||||
pass
|
||||
except WebSocketDisconnect:
|
||||
if _active_runtime:
|
||||
_active_runtime.detach_ws()
|
||||
|
||||
@ -523,7 +523,7 @@ class FrameEngine:
|
||||
return self._make_result(result)
|
||||
|
||||
# Complex action — needs full pipeline
|
||||
self._end_frame(rec, output_summary="no local handler", route="director/thinker")
|
||||
self._end_frame(rec, output_summary="no local handler", route="pa/director/thinker")
|
||||
|
||||
action_desc = f"ACTION: {action}"
|
||||
if data:
|
||||
@ -535,7 +535,9 @@ class FrameEngine:
|
||||
analysis=InputAnalysis(intent="action", topic=action, complexity="simple"),
|
||||
source_text=action_desc)
|
||||
|
||||
if self.has_director:
|
||||
if self.has_pa:
|
||||
return await self._run_expert_pipeline(command, mem_ctx, dashboard)
|
||||
elif self.has_director:
|
||||
return await self._run_director_pipeline(command, mem_ctx, dashboard)
|
||||
else:
|
||||
return await self._run_thinker_pipeline(command, mem_ctx, dashboard)
|
||||
|
||||
@ -9,15 +9,21 @@ 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) {
|
||||
if (!_nodeState[name]) {
|
||||
_nodeState[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[name];
|
||||
return _nodeState[key];
|
||||
}
|
||||
|
||||
export function updateNodeFromHud(node, event, data) {
|
||||
@ -74,15 +80,23 @@ export function updateNodeFromHud(node, event, data) {
|
||||
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) { console.warn('[nodes] #node-metrics not found'); return; }
|
||||
if (!el) return;
|
||||
|
||||
// Sort: active nodes first, then by name
|
||||
const statusOrder = { thinking: 0, tool: 0, streaming: 0, planned: 1, done: 2, idle: 3 };
|
||||
const sorted = Object.entries(_nodeState)
|
||||
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine')
|
||||
.sort((a, b) => (statusOrder[a[1].status] || 3) - (statusOrder[b[1].status] || 3));
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user