Files
box/bin/agent-cognitive-probe.py
T

655 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
"""agent-cognitive-probe.py — Real-time Cognitive Sensing and Agent Menu Navigation.
Directly probes Cloud Muse / Hatch browser runtime via CDP:
1. Passive Cognitive Sensing (zero-click):
- Token streaming / generation state (stop button presence)
- Typing / thinking indicators
- Status text displayed under/beside avatar (Connected, Thinking, Working)
- Active context (Main chat vs Side chats with thread titles & snippets)
- Input wait / parked approval detection
2. Active Profile Menu Navigation:
- Status panel sliding surface navigation
- Tabs: Activity (tasks & processes), Upcoming (timers & recurring cron loops),
Approvals, and Identity.
3. Cognitive Lock Gate:
- Protects single-threaded thought process from interruptions.
"""
import argparse
import json
import os
import subprocess
import sys
import time
import urllib.request
# Node to pinned CDP port mapping
NODE_CDP_PORTS = {
"muse": 9410,
"pip": 9420,
"646": 9430,
"opm": 9440,
"def": 9450,
"dev": 9455,
"muse-main": 9410,
}
REMOTE_HOST = "100.123.153.75" # bl control node
def is_running_on_bl():
"""Detect if we are running locally on bl or on tp/remote."""
try:
import socket
hn = socket.gethostname().lower()
if "bl" in hn:
return True
except Exception:
pass
# Check if network namespaces exist locally
return os.path.exists("/var/run/netns/warp-muse") or os.path.exists("/run/netns/warp-muse")
def run_cdp_eval_inside_netns(node, js_code, timeout=8):
"""Executes a JS snippet against the node's browser via CDP inside its netns."""
port = NODE_CDP_PORTS.get(node)
if not port:
return {"error": f"Unknown node '{node}'"}
# Python runner script to execute inside the target environment
runner_code = f'''
import json, sys, urllib.request
try:
import websocket
except ImportError:
print(json.dumps({{"error": "websocket package missing"}}))
sys.exit(1)
try:
with urllib.request.urlopen("http://127.0.0.1:{port}/json/list", timeout=3) as r:
targets = json.load(r)
pages = [t for t in targets if t.get("type") == "page"]
if not pages:
print(json.dumps({{"error": "No active page target"}}))
sys.exit(0)
ws_url = pages[0]["webSocketDebuggerUrl"]
ws = websocket.create_connection(ws_url, timeout={timeout})
ws.send(json.dumps({{
"id": 1,
"method": "Runtime.evaluate",
"params": {{
"expression": {json.dumps(js_code)},
"returnByValue": True,
"awaitPromise": True
}}
}}))
res = None
for _ in range(30):
msg = json.loads(ws.recv())
if msg.get("id") == 1:
res = msg.get("result", {{}}).get("result", {{}}).get("value")
break
print(json.dumps({{"ok": True, "value": res}}))
except Exception as e:
print(json.dumps({{"error": str(e)}}))
'''
if is_running_on_bl():
cmd = ["sudo", "ip", "netns", "exec", f"warp-{node}", "python3", "-c", runner_code]
else:
# Wrap via ssh to bl
# Use python3 on bl directly executing inside netns
remote_cmd = f"sudo ip netns exec warp-{node} python3 -c {subprocess.list2cmdline([runner_code])}"
cmd = ["ssh", "-q", f"super@{REMOTE_HOST}", remote_cmd]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 5)
if proc.returncode != 0 and not proc.stdout:
return {"error": proc.stderr.strip() or f"Process exited with {proc.returncode}"}
# Parse output line that contains valid json
for line in proc.stdout.strip().splitlines():
line = line.strip()
if line.startswith("{") and line.endswith("}"):
try:
data = json.loads(line)
if "ok" in data:
return data["value"]
if "error" in data:
return {"error": data["error"]}
except Exception:
continue
return {"error": proc.stdout.strip() or proc.stderr.strip()}
except subprocess.TimeoutExpired:
return {"error": "CDP probe timed out"}
except Exception as e:
return {"error": str(e)}
JS_PASSIVE_COGNITIVE = """(() => {
// 1. Generation & Thinking signals
const stopBtn = document.querySelector('[data-testid="hatch-composer-stop-button"]');
const isGenerating = !!stopBtn;
const typingEl = document.querySelector('[data-testid="hatch-chat-typing-indicator"]');
const isTyping = !!typingEl && typingEl.textContent.trim().length > 0;
// 2. Avatar / Status text
const statusTextEl = document.querySelector('.group\\\\/status-avatar span, [class*="status-avatar"] span, span[class*="text-body-status"]');
const avatarStatus = statusTextEl ? (statusTextEl.innerText || '').trim() : '';
// 3. Thread context & active URL
const url = window.location.href;
const isMainChat = url === 'https://muse.ai/' || url.endsWith('/thread/new');
const pageTitle = document.title;
// 4. Side chats overview
const sideRows = Array.from(document.querySelectorAll('[data-testid="hatch-thread-row"]')).map(r => {
const text = (r.innerText || '').trim().replace(/\\\\n+/g, ' — ');
return text;
}).slice(0, 5);
// 5. Input wait / Parked prompt cards
// Detect buttons asking for approval / input in the message flow
const actionBtns = Array.from(document.querySelectorAll('div[data-message-id] button')).map(b => (b.innerText || '').trim()).filter(t => /approve|confirm|proceed|resume|start|allow/i.test(t));
const isInputWait = actionBtns.length > 0 || /asking for input|pending approval/i.test(document.body.innerText.slice(-600));
// 6. Status panel state
const panel = document.querySelector('[data-testid="hatch-status-panel-sliding-surface"]');
const panelOpen = !!panel && panel.getBoundingClientRect().width > 0;
return {
url: url,
title: pageTitle,
is_main_chat: isMainChat,
is_generating: isGenerating,
is_typing: isTyping,
avatar_status: avatarStatus,
is_input_wait: isInputWait,
pending_actions: actionBtns,
panel_open: panelOpen,
side_chats: sideRows
};
})()"""
def get_passive_cognitive_state(node):
"""Gathers passive cognitive signals without altering UI state."""
if not is_running_on_bl():
try:
cmd = ["ssh", "-q", "-o", "ConnectTimeout=5", f"super@{REMOTE_HOST}",
f"python3 /home/super/Projects/NetVM/bin/agent-cognitive-probe.py status {node} --json"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if proc.returncode == 0 and proc.stdout.strip():
data = json.loads(proc.stdout.strip())
if isinstance(data, list) and len(data) > 0:
return data[0]
elif isinstance(data, dict):
return data
except Exception as e:
return {
"node": node,
"status": "DARK",
"error": f"Remote delegation failed: {e}",
"cognitive_lock": False,
"lock_reason": None,
}
raw = run_cdp_eval_inside_netns(node, JS_PASSIVE_COGNITIVE)
if not isinstance(raw, dict) or "error" in raw:
return {
"node": node,
"status": "DARK",
"error": raw.get("error", "Unknown error") if isinstance(raw, dict) else str(raw),
"cognitive_lock": False,
"lock_reason": None,
}
is_generating = raw.get("is_generating", False)
is_typing = raw.get("is_typing", False)
is_input_wait = raw.get("is_input_wait", False)
avatar_status = raw.get("avatar_status", "")
is_main = raw.get("is_main_chat", True)
# Determine synthesized cognitive state
if is_generating or is_typing or "thinking" in avatar_status.lower():
state = "THINKING"
locked = True
reason = "Agent is actively generating tokens / thinking (stop button active)"
elif is_input_wait:
state = "INPUT_WAIT"
locked = True
reason = "Agent is waiting for operator or system input on a parked prompt"
elif "working" in avatar_status.lower() or "making" in avatar_status.lower():
state = "WORKING"
locked = True
reason = f"Avatar status indicates work in progress: '{avatar_status}'"
elif not is_main:
state = "SIDECHAT_IDLE"
locked = False
reason = None
else:
state = "IDLE"
locked = False
reason = None
return {
"node": node,
"status": state,
"cognitive_lock": locked,
"lock_reason": reason,
"details": raw,
}
JS_NAVIGATE_MENU_TEMPLATE = """(async () => {
// 1. Ensure panel is open
let panel = document.querySelector('[data-testid="hatch-status-panel-sliding-surface"]');
if (!panel) {
// Try clicking avatar or trigger
const avatarBtn = document.querySelector('[role="img"][aria-label*="avatar"], button[aria-label*="avatar"], .group\\\\/status-avatar');
if (avatarBtn) avatarBtn.click();
await new Promise(r => setTimeout(r, 350));
}
// 2. Click requested tab if specified
const targetTab = "%(tab)s";
if (targetTab && targetTab !== "all") {
const btn = document.querySelector('button[aria-label="' + targetTab + '"]');
if (btn) {
btn.click();
await new Promise(r => setTimeout(r, 400));
}
}
panel = document.querySelector('[data-testid="hatch-status-panel-sliding-surface"]');
if (!panel) return {error: "Status panel not rendered"};
// Read full text and structural items
const rawText = panel.innerText || '';
const lines = rawText.split('\\n').map(s => s.trim()).filter(Boolean);
// Extract activity / timer items
const items = [];
const buttons = Array.from(panel.querySelectorAll('button')).filter(b => !b.getAttribute('aria-label') && (b.innerText || '').length > 0);
buttons.forEach(b => {
const text = (b.innerText || '').trim();
const parts = text.split('\\n').map(s => s.trim()).filter(Boolean);
if (parts.length >= 2) {
items.push({
title: parts[0],
detail: parts[1],
time: parts.length > 2 ? parts[2] : null
});
}
});
return {
raw_text: rawText,
lines: lines,
items: items
};
})()"""
def get_agent_menu(node, tab="all"):
"""Navigates and extracts data from the agent profile / status panel."""
if not is_running_on_bl():
try:
cmd = ["ssh", "-q", "-o", "ConnectTimeout=5", f"super@{REMOTE_HOST}",
f"python3 /home/super/Projects/NetVM/bin/agent-cognitive-probe.py menu {node} {tab} --json"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if proc.returncode == 0 and proc.stdout.strip():
return json.loads(proc.stdout.strip())
except Exception as e:
return {
"node": node,
"tab": tab,
"data": {"error": f"Remote delegation failed: {e}"}
}
tab_map = {
"activity": "Activity",
"upcoming": "Upcoming",
"approvals": "Approvals",
"identity": "Identity",
"all": "all",
}
target_tab = tab_map.get(tab.lower(), "Activity")
# If all is requested, gather activity, upcoming, and approvals
if target_tab == "all":
result = {}
for sub_tab in ["Activity", "Upcoming", "Approvals"]:
js = JS_NAVIGATE_MENU_TEMPLATE % {"tab": sub_tab}
tab_res = run_cdp_eval_inside_netns(node, js)
result[sub_tab.lower()] = tab_res
return {
"node": node,
"menu": result
}
js = JS_NAVIGATE_MENU_TEMPLATE % {"tab": target_tab}
res = run_cdp_eval_inside_netns(node, js)
return {
"node": node,
"tab": target_tab,
"data": res
}
JS_LIVE_SCREEN = """(() => {
const ps = Array.from(document.querySelectorAll('p')).map(p => (p.innerText || '').trim()).filter(Boolean);
const actionBtns = Array.from(document.querySelectorAll('div[data-message-id] button, [role="log"] button, div[role="status"] button')).map(b => (b.innerText || '').trim()).filter(Boolean);
const stopBtn = !!document.querySelector('[data-testid="hatch-composer-stop-button"]');
const typing = !!document.querySelector('[data-testid="hatch-chat-typing-indicator"]:not(:empty)');
const statusTextEl = document.querySelector('.group\\\\/status-avatar span, [class*="status-avatar"] span, span[class*="text-body-status"]');
const avatarStatus = statusTextEl ? (statusTextEl.innerText || '').trim() : '';
return {
title: document.title,
url: window.location.href,
is_main_chat: window.location.href === 'https://muse.ai/' || window.location.href.endsWith('/thread/new'),
is_generating: stopBtn,
is_typing: typing,
avatar_status: avatarStatus,
recent_paragraphs: ps.slice(-8),
action_buttons: actionBtns.filter(t => /approve|confirm|proceed|resume|start|allow|review/i.test(t))
};
})()"""
def get_agent_live_screen(node: str) -> dict:
"""Extracts live active chat text, thoughts, prompt blocks, and threads."""
if not is_running_on_bl():
try:
cmd = ["ssh", "-q", "-o", "ConnectTimeout=5", f"super@{REMOTE_HOST}",
f"python3 /home/super/Projects/NetVM/bin/agent-cognitive-probe.py read {node} --json"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if proc.returncode == 0 and proc.stdout.strip():
return json.loads(proc.stdout.strip())
except Exception as e:
return {"node": node, "error": f"Remote delegation failed: {e}"}
screen = run_cdp_eval_inside_netns(node, JS_LIVE_SCREEN)
if not isinstance(screen, dict) or "error" in screen:
return {"node": node, "error": screen.get("error", "Failed to inspect screen") if isinstance(screen, dict) else str(screen)}
sidechats = []
try:
cli_cmd = ["/home/super/Projects/NetVM/bin/muse-cli-node", node, "threads"]
proc = subprocess.run(cli_cmd, capture_output=True, text=True, timeout=8)
if proc.returncode == 0 and proc.stdout.strip():
threads_data = json.loads(proc.stdout.strip())
if isinstance(threads_data, list):
sidechats = threads_data[:8]
except Exception:
pass
return {
"node": node,
"screen": screen,
"sidechats": sidechats
}
JS_NAVIGATE_MAIN_CHAT = """(() => {
try {
const url = window.location.href;
if (url === 'https://muse.ai/' || url === 'https://muse.ai/thread/new') {
return { ok: true, already_main: true };
}
const candidates = Array.from(document.querySelectorAll('a, button, [role="button"], [data-testid]'));
const mainBtn = candidates.find(b => {
const t = (b.innerText || '').trim().toLowerCase();
return t === 'main chat' || b.getAttribute('aria-label') === 'Main chat' || b.getAttribute('data-testid') === 'hatch-sidebar-main-chat';
});
if (mainBtn) {
mainBtn.click();
return { ok: true, method: 'click' };
}
window.location.href = 'https://muse.ai/';
return { ok: true, method: 'navigate' };
} catch (e) {
return { error: String(e) };
}
})()"""
def navigate_to_main_chat(node: str) -> dict:
"""Navigates the agent browser session back to Main Chat (https://muse.ai/)."""
if not is_running_on_bl():
try:
cmd = ["ssh", "-q", "-o", "ConnectTimeout=5", f"super@{REMOTE_HOST}",
f"python3 /home/super/Projects/NetVM/bin/agent-cognitive-probe.py nav-main {node} --json"]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if proc.returncode == 0 and proc.stdout.strip():
return json.loads(proc.stdout.strip())
except Exception as e:
return {"error": str(e)}
return run_cdp_eval_inside_netns(node, JS_NAVIGATE_MAIN_CHAT)
def cmd_nav_main(args):
node = getattr(args, "agent", None) or getattr(args, "node", None)
res = navigate_to_main_chat(node)
if getattr(args, "json", False):
print(json.dumps(res, indent=2))
else:
if isinstance(res, dict) and res.get("ok"):
m = res.get("method") or ("already in main chat" if res.get("already_main") else "default")
print(f"✓ Refocused agent '{node}' to Main Chat ({m})")
else:
err = res.get("error") if isinstance(res, dict) else str(res)
print(f"❌ Failed to refocus '{node}' to Main Chat: {err}")
def cmd_read(args):
node = getattr(args, "agent", None) or getattr(args, "node", None)
data = get_agent_live_screen(node)
if getattr(args, "json", False):
print(json.dumps(data, indent=2))
return
if "error" in data:
print(f"\n❌ Error inspecting screen for {node}: {data['error']}\n")
return
screen = data.get("screen", {})
sidechats = data.get("sidechats", [])
url = screen.get("url", "")
mode = "Main Chat" if screen.get("is_main_chat") else "Side Chat"
title = screen.get("title", "")
avatar = screen.get("avatar_status") or "Connected"
gen = "🧠 GENERATING" if screen.get("is_generating") else ("💭 TYPING" if screen.get("is_typing") else "🟢 SETTLED")
print(f"\n=== LIVE THOUGHT STREAM & ACTIVE CHAT: {node.upper()} ===")
print(f"Context: {mode} ({url})")
print(f"Title: {title}")
print(f"Status: {avatar} | State: {gen}")
paragraphs = screen.get("recent_paragraphs", [])
if paragraphs:
print("\n--- ACTIVE CONVERSATION & THOUGHT PARAGRAPHS ---")
for p in paragraphs:
print(f" • {p}\n")
else:
print("\n (No text paragraphs visible in current viewport)")
actions = screen.get("action_buttons", [])
if actions:
print("--- PENDING ACTION CARDS / APPROVAL BUTTONS ---")
for a in actions:
print(f" ⚠️ [PROMPT ACTION] {a}")
print()
if sidechats:
print("--- RECENT SIDE CHATS & TOPICS ---")
for sc in sidechats:
sid = sc.get("session_id", "")[:8]
stitle = sc.get("title") or "(Untitled sidechat)"
upd = sc.get("updated", "")
print(f" • [{sid}] {stitle} ({upd})")
print()
def format_cognitive_badge(status):
badges = {
"IDLE": "🟢 IDLE",
"SIDECHAT_IDLE": "💬 SIDE_IDLE",
"THINKING": "🧠 THINKING",
"INPUT_WAIT": "⏸️ INPUT_WAIT",
"BUSY_SIDECHAT": "💬 SIDECHAT",
"WORKING": "⚙️ WORKING",
"DARK": "⚫ DARK",
}
return badges.get(status, f"❓ {status}")
def cmd_status(args):
nodes = getattr(args, "agents", None) or getattr(args, "nodes", None) or ["muse", "pip", "646", "opm", "dev", "def"]
results = []
for n in nodes:
results.append(get_passive_cognitive_state(n))
if getattr(args, "json", False):
print(json.dumps(results, indent=2))
return
print("\n=== AGENT COGNITIVE SENSOR (LIVE DOM PROBE) ===")
print(f"{'AGENT':<10} {'COGNITIVE STATE':<18} {'LOCK':<8} {'UNDER-AVATAR':<15} {'DETAILS / CURRENT THOUGHT':<40}")
print("-" * 95)
for r in results:
node = r["node"]
status = r["status"]
badge = format_cognitive_badge(status)
locked = "LOCKED" if r.get("cognitive_lock") else "OPEN"
det = r.get("details", {})
avatar_status = det.get("avatar_status", "-")
reason = r.get("lock_reason") or det.get("title", "Settled")
if status == "DARK":
reason = r.get("error", "CDP unreachable")
avatar_status = "DARK"
print(f"{node:<10} {badge:<18} {locked:<8} {avatar_status:<15} {reason[:40]:<40}")
print()
def cmd_menu(args):
node = getattr(args, "agent", None) or getattr(args, "node", None)
tab = getattr(args, "tab", "activity") or "activity"
data = get_agent_menu(node, tab)
if getattr(args, "json", False):
print(json.dumps(data, indent=2))
return
print(f"\n=== AGENT MENU: {node.upper()} (TAB: {tab.upper()}) ===")
if tab.lower() == "all":
menu = data.get("menu", {})
for tname, tdata in menu.items():
print(f"\n--- {tname.upper()} ---")
if isinstance(tdata, dict) and "error" in tdata:
print(f" Error: {tdata['error']}")
elif isinstance(tdata, dict):
items = tdata.get("items", [])
if items:
for it in items:
t_str = f" [{it['time']}]" if it.get("time") else ""
print(f" • {it['title']}: {it['detail']}{t_str}")
else:
raw = tdata.get("raw_text", "")
for line in raw.split("\n"):
if line.strip():
print(f" {line.strip()}")
print()
return
tdata = data.get("data", {})
if isinstance(tdata, dict) and "error" in tdata:
print(f" Error: {tdata['error']}")
elif isinstance(tdata, dict):
items = tdata.get("items", [])
if items:
for it in items:
t_str = f" [{it['time']}]" if it.get("time") else ""
print(f" • {it['title']}: {it['detail']}{t_str}")
else:
raw = tdata.get("raw_text", "")
for line in raw.split("\n"):
if line.strip():
print(f" {line.strip()}")
print()
def cmd_lock(args):
node = getattr(args, "agent", None) or getattr(args, "node", None)
state = get_passive_cognitive_state(node)
if args.json:
print(json.dumps(state, indent=2))
else:
if state.get("cognitive_lock"):
print(f"🔴 COGNITIVE LOCK ENGAGED on '{node}' ({state['status']}): {state.get('lock_reason')}")
else:
print(f"🟢 COGNITIVELY IDLE: Agent '{node}' is free to receive new work without interruption.")
if state.get("cognitive_lock") and not getattr(args, "force", False):
sys.exit(1)
sys.exit(0)
def main():
parser = argparse.ArgumentParser(description="Real-time Cognitive Sensing and Agent Menu Navigation")
sub = parser.add_subparsers(dest="command")
p_status = sub.add_parser("status", help="Show cognitive state for all or selected agents")
p_status.add_argument("nodes", nargs="*", help="Optional agent names")
p_status.add_argument("--json", action="store_true", help="Output JSON")
p_menu = sub.add_parser("menu", help="Navigate agent profile menu (tasks, timers, approvals, identity)")
p_menu.add_argument("node", help="Agent name (muse, pip, 646, opm, dev, def)")
p_menu.add_argument("tab", nargs="?", default="activity", choices=["activity", "upcoming", "approvals", "identity", "all"], help="Menu tab to view")
p_menu.add_argument("--json", action="store_true", help="Output JSON")
p_lock = sub.add_parser("lock", help="Check cognitive lock before dispatching work")
p_lock.add_argument("node", help="Agent name")
p_lock.add_argument("--force", action="store_true", help="Bypass lock check")
p_lock.add_argument("--json", action="store_true", help="Output JSON")
p_read = sub.add_parser("read", help="Extract live active chat text, thought stream, and side chats")
p_read.add_argument("node", help="Agent name")
p_read.add_argument("--json", action="store_true", help="Output JSON")
p_nav = sub.add_parser("nav-main", help="Navigate agent browser session back to Main Chat")
p_nav.add_argument("node", help="Agent name")
p_nav.add_argument("--json", action="store_true", help="Output JSON")
args = parser.parse_args()
if not args.command:
# Default to status
args.nodes = []
args.json = False
cmd_status(args)
return
if args.command == "status":
cmd_status(args)
elif args.command == "menu":
cmd_menu(args)
elif args.command == "read":
cmd_read(args)
elif args.command == "lock":
cmd_lock(args)
elif args.command == "nav-main":
cmd_nav_main(args)
if __name__ == "__main__":
main()