feat(cognitive): implement real-time cognitive sensing, menu navigation, and single-task lock
This commit is contained in:
Executable
+484
@@ -0,0 +1,484 @@
|
||||
#!/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 not is_main:
|
||||
state = "BUSY_SIDECHAT"
|
||||
locked = True
|
||||
reason = f"Agent is active in sidechat: {raw.get('title', 'Side Chat')}"
|
||||
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}'"
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
def format_cognitive_badge(status):
|
||||
badges = {
|
||||
"IDLE": "🟢 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")
|
||||
|
||||
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 == "lock":
|
||||
cmd_lock(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
agent-cognitive-probe.py
|
||||
+74
-4
@@ -26,6 +26,11 @@ import hashlib
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import agent_cognitive_probe as acp
|
||||
except ImportError:
|
||||
acp = None
|
||||
|
||||
# Color helpers
|
||||
USE_COLOR = sys.stdout.isatty() or os.environ.get("CLICOLOR_FORCE") == "1"
|
||||
|
||||
@@ -495,7 +500,7 @@ def cmd_status(args):
|
||||
uname = assignee.get("username")
|
||||
agent_active_issues[uname] = iss
|
||||
|
||||
headers = f"{'AGENT':<12} {'ROLE':<13} {'PORT':<6} {'TUNNEL':<8} {'SIGNAL':<10} {'ACTIVE WORK / ASSIGNMENT':<38} {'LAST CHAT'}"
|
||||
headers = f"{'AGENT':<10} {'ROLE':<12} {'PORT':<6} {'TUNNEL':<7} {'COGNITIVE':<15} {'ACTIVE WORK / ASSIGNMENT':<38} {'LAST CHAT'}"
|
||||
print(c_dim(headers))
|
||||
print(c_dim("-" * len(headers)))
|
||||
|
||||
@@ -512,6 +517,15 @@ def cmd_status(args):
|
||||
active_task = claimed_tasks.get(name)
|
||||
active_issue = agent_active_issues.get(name)
|
||||
|
||||
# Live cognitive probe
|
||||
cog_badge = "-"
|
||||
if acp and name != "muse-main":
|
||||
cog = acp.get_passive_cognitive_state(name)
|
||||
cog_status = cog.get("status", "DARK")
|
||||
cog_badge = acp.format_cognitive_badge(cog_status)
|
||||
elif name == "muse-main":
|
||||
cog_badge = c_dim("HOST")
|
||||
|
||||
if active_issue:
|
||||
num = active_issue.get("number")
|
||||
title = active_issue.get("title", "")[:32]
|
||||
@@ -542,7 +556,7 @@ def cmd_status(args):
|
||||
else:
|
||||
chat_str = c_dim("-")
|
||||
|
||||
print(f"{c_bold(name):<21} {role:<13} {port:<6} {tunnel_str:<17} {signal:<19} {work_desc:<38} {chat_str}")
|
||||
print(f"{c_bold(name):<19} {role:<12} {port:<6} {tunnel_str:<16} {cog_badge:<24} {work_desc:<38} {chat_str}")
|
||||
|
||||
print()
|
||||
|
||||
@@ -644,7 +658,17 @@ def cmd_start(args):
|
||||
agent = args.agent
|
||||
body = args.goal or f"Work task for {agent}: {title}"
|
||||
|
||||
# 0. Pre-flight health gate: Hatch, Restore, Git Config with Auto-Heal
|
||||
# 0a. Real-time Cognitive Sensing & Single-Task Lock Gate
|
||||
if acp and not getattr(args, "force", False):
|
||||
cog = acp.get_passive_cognitive_state(agent)
|
||||
if cog.get("cognitive_lock"):
|
||||
print(c_red(f"\n[COGNITIVE LOCK BLOCKED] Agent '{agent}' cannot receive new work right now ({cog['status']}):"))
|
||||
print(f" • Reason: {cog.get('lock_reason')}")
|
||||
print(c_yellow("\nCloud agents have single-threaded thought processes and get interrupted by concurrent tasks."))
|
||||
print(c_dim(f"To inspect active thoughts/menu: box work menu {agent}\nTo bypass lock: box work start '{title}' --to {agent} --force\n"))
|
||||
sys.exit(1)
|
||||
|
||||
# 0b. Pre-flight health gate: Hatch, Restore, Git Config with Auto-Heal
|
||||
preflight = check_agent_preflight(agent)
|
||||
if not preflight["ready"] and not getattr(args, "force", False):
|
||||
if getattr(args, "no_heal", False):
|
||||
@@ -734,7 +758,17 @@ def cmd_assign(args):
|
||||
issue_num = args.issue
|
||||
agent = args.agent
|
||||
|
||||
# 0. Pre-flight health gate: Hatch, Restore, Git Config with Auto-Heal
|
||||
# 0a. Real-time Cognitive Sensing & Single-Task Lock Gate
|
||||
if acp and not getattr(args, "force", False):
|
||||
cog = acp.get_passive_cognitive_state(agent)
|
||||
if cog.get("cognitive_lock"):
|
||||
print(c_red(f"\n[COGNITIVE LOCK BLOCKED] Agent '{agent}' cannot receive new work right now ({cog['status']}):"))
|
||||
print(f" • Reason: {cog.get('lock_reason')}")
|
||||
print(c_yellow("\nCloud agents have single-threaded thought processes and get interrupted by concurrent tasks."))
|
||||
print(c_dim(f"To inspect active thoughts/menu: box work menu {agent}\nTo bypass lock: box work assign {issue_num} --to {agent} --force\n"))
|
||||
sys.exit(1)
|
||||
|
||||
# 0b. Pre-flight health gate: Hatch, Restore, Git Config with Auto-Heal
|
||||
preflight = check_agent_preflight(agent)
|
||||
if not preflight["ready"] and not getattr(args, "force", False):
|
||||
if getattr(args, "no_heal", False):
|
||||
@@ -817,9 +851,23 @@ def cmd_chats(args):
|
||||
print(f"[{c_cyan(ag)} : {c_dim(tname)}] {c_dim(ts)} {c_bold(author)}:\n{text}\n" + c_dim("-" * 60))
|
||||
print()
|
||||
|
||||
def cmd_menu(args):
|
||||
if not acp:
|
||||
print(c_red("Error: agent_cognitive_probe module not found."))
|
||||
sys.exit(1)
|
||||
acp.cmd_menu(args)
|
||||
|
||||
def cmd_cognitive(args):
|
||||
if not acp:
|
||||
print(c_red("Error: agent_cognitive_probe module not found."))
|
||||
sys.exit(1)
|
||||
acp.cmd_status(args)
|
||||
|
||||
WORK_COMMAND_EXAMPLES = {
|
||||
"box work": [
|
||||
"box work # View fleet workspace dashboard & signals",
|
||||
"box work cognitive [agent...] # Live zero-click cognitive sensor probe across fleet",
|
||||
"box work menu <agent> [tab] # Inspect agent profile menu (tasks, timers, approvals)",
|
||||
"box work check [agent] # Audit pre-flight health gates",
|
||||
"box work heal <agent> # Automated remediation & chat nudge",
|
||||
"box work start \"<title>\" --to <agent> # Start & dispatch new build ticket",
|
||||
@@ -827,6 +875,15 @@ WORK_COMMAND_EXAMPLES = {
|
||||
"box work merge <pr#> # Verify tests and merge PR to master",
|
||||
"box work chats --agent <name> # View live multi-agent chat feed",
|
||||
],
|
||||
"box work menu": [
|
||||
"box work menu muse upcoming # Inspect timers & recurring cron loops",
|
||||
"box work menu 646 activity # Inspect recent tasks & active processes",
|
||||
"box work menu pip all # Dump all tabs (activity, upcoming, approvals)",
|
||||
],
|
||||
"box work cognitive": [
|
||||
"box work cognitive # Live cognitive sensor probe for all agents",
|
||||
"box work cognitive pip # Check if pip is generating / thinking",
|
||||
],
|
||||
"box work start": [
|
||||
"box work start \"Fix SSH perms\" --to 646",
|
||||
"box work start \"Build integration tests\" --to pip --goal \"Run pytest on endpoints\"",
|
||||
@@ -960,6 +1017,15 @@ def main():
|
||||
p_chats.add_argument("--agent", help="Filter by agent name")
|
||||
p_chats.add_argument("--limit", type=int, default=10, help="Number of messages to show")
|
||||
|
||||
p_menu = sub.add_parser("menu", help="Navigate agent profile menu (tasks, timers, approvals, identity)")
|
||||
p_menu.add_argument("agent", help="Agent username (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_cog = sub.add_parser("cognitive", help="Probe real-time cognitive sensor (thinking, generating, sidechats)")
|
||||
p_cog.add_argument("agents", nargs="*", help="Optional agent usernames")
|
||||
p_cog.add_argument("--json", action="store_true", help="Output JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
action = args.work_action
|
||||
|
||||
@@ -977,6 +1043,10 @@ def main():
|
||||
cmd_merge(args)
|
||||
elif action == "chats":
|
||||
cmd_chats(args)
|
||||
elif action == "menu":
|
||||
cmd_menu(args)
|
||||
elif action == "cognitive":
|
||||
cmd_cognitive(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
@@ -1489,6 +1489,10 @@ def cmd_work(args):
|
||||
box_work.cmd_heal(args)
|
||||
elif action == "chats":
|
||||
box_work.cmd_chats(args)
|
||||
elif action == "menu":
|
||||
box_work.cmd_menu(args)
|
||||
elif action == "cognitive":
|
||||
box_work.cmd_cognitive(args)
|
||||
else:
|
||||
box_work.cmd_status(args)
|
||||
|
||||
@@ -7004,6 +7008,11 @@ def build_parser():
|
||||
p_w_chats = work_sub.add_parser("chats", parents=[common], help="View recent live chat activity")
|
||||
p_w_chats.add_argument("--agent", help="Filter by agent name")
|
||||
p_w_chats.add_argument("--limit", type=int, default=10, help="Number of messages to show")
|
||||
p_w_menu = work_sub.add_parser("menu", parents=[common], help="Navigate agent profile menu (tasks, timers, approvals, identity)")
|
||||
p_w_menu.add_argument("agent", help="Agent username (muse, pip, 646, opm, dev, def)")
|
||||
p_w_menu.add_argument("tab", nargs="?", default="activity", choices=["activity", "upcoming", "approvals", "identity", "all"], help="Menu tab to view")
|
||||
p_w_cog = work_sub.add_parser("cognitive", parents=[common], help="Probe real-time cognitive sensor (thinking, generating, sidechats)")
|
||||
p_w_cog.add_argument("agents", nargs="*", help="Optional agent usernames")
|
||||
|
||||
p_tasks = subparsers.add_parser("tasks", parents=[common], help="Agent work queue: pending/claimed/done files (distinct from scheduled jobs)")
|
||||
p_tasks.add_argument("--dir", default=None, help="Task queue dir (default: fleet/tasks)")
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""test_box_cognitive.py — Unit tests for agent cognitive sensing and menu navigation."""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "bin"))
|
||||
import agent_cognitive_probe as acp
|
||||
import box_work
|
||||
|
||||
|
||||
class TestAgentCognitiveProbe(unittest.TestCase):
|
||||
|
||||
def test_badge_formatting(self):
|
||||
self.assertIn("IDLE", acp.format_cognitive_badge("IDLE"))
|
||||
self.assertIn("THINKING", acp.format_cognitive_badge("THINKING"))
|
||||
self.assertIn("INPUT_WAIT", acp.format_cognitive_badge("INPUT_WAIT"))
|
||||
self.assertIn("SIDECHAT", acp.format_cognitive_badge("BUSY_SIDECHAT"))
|
||||
self.assertIn("DARK", acp.format_cognitive_badge("DARK"))
|
||||
|
||||
@patch("agent_cognitive_probe.is_running_on_bl", return_value=True)
|
||||
@patch("agent_cognitive_probe.run_cdp_eval_inside_netns")
|
||||
def test_passive_thinking_state(self, mock_cdp, mock_bl):
|
||||
# Simulate active token generation (stop button present)
|
||||
mock_cdp.return_value = {
|
||||
"is_generating": True,
|
||||
"is_typing": False,
|
||||
"avatar_status": "Connected",
|
||||
"is_main_chat": True,
|
||||
"title": "Chat — test",
|
||||
"is_input_wait": False
|
||||
}
|
||||
res = acp.get_passive_cognitive_state("pip")
|
||||
self.assertEqual(res["status"], "THINKING")
|
||||
self.assertTrue(res["cognitive_lock"])
|
||||
self.assertIn("stop button active", res["lock_reason"])
|
||||
|
||||
@patch("agent_cognitive_probe.is_running_on_bl", return_value=True)
|
||||
@patch("agent_cognitive_probe.run_cdp_eval_inside_netns")
|
||||
def test_passive_input_wait_state(self, mock_cdp, mock_bl):
|
||||
# Simulate parked approval card
|
||||
mock_cdp.return_value = {
|
||||
"is_generating": False,
|
||||
"is_typing": False,
|
||||
"avatar_status": "Connected",
|
||||
"is_main_chat": True,
|
||||
"title": "Chat — test",
|
||||
"is_input_wait": True
|
||||
}
|
||||
res = acp.get_passive_cognitive_state("646")
|
||||
self.assertEqual(res["status"], "INPUT_WAIT")
|
||||
self.assertTrue(res["cognitive_lock"])
|
||||
|
||||
@patch("agent_cognitive_probe.is_running_on_bl", return_value=True)
|
||||
@patch("agent_cognitive_probe.run_cdp_eval_inside_netns")
|
||||
def test_passive_idle_state(self, mock_cdp, mock_bl):
|
||||
# Simulate fully settled idle state
|
||||
mock_cdp.return_value = {
|
||||
"is_generating": False,
|
||||
"is_typing": False,
|
||||
"avatar_status": "Connected",
|
||||
"is_main_chat": True,
|
||||
"title": "Chat — test",
|
||||
"is_input_wait": False
|
||||
}
|
||||
res = acp.get_passive_cognitive_state("dev")
|
||||
self.assertEqual(res["status"], "IDLE")
|
||||
self.assertFalse(res["cognitive_lock"])
|
||||
self.assertIsNone(res["lock_reason"])
|
||||
|
||||
@patch("agent_cognitive_probe.is_running_on_bl", return_value=True)
|
||||
@patch("agent_cognitive_probe.run_cdp_eval_inside_netns")
|
||||
def test_menu_upcoming_timers(self, mock_cdp, mock_bl):
|
||||
# Simulate Upcoming tab content
|
||||
mock_cdp.return_value = {
|
||||
"raw_text": "Fleet sync loop\nEvery 15 minutes\nHeartbeat\nEvery 30 minutes",
|
||||
"lines": ["Fleet sync loop", "Every 15 minutes", "Heartbeat", "Every 30 minutes"],
|
||||
"items": [
|
||||
{"title": "Fleet sync loop", "detail": "Every 15 minutes", "time": None},
|
||||
{"title": "Heartbeat", "detail": "Every 30 minutes", "time": None}
|
||||
]
|
||||
}
|
||||
res = acp.get_agent_menu("muse", tab="upcoming")
|
||||
self.assertEqual(res["node"], "muse")
|
||||
self.assertEqual(res["tab"], "Upcoming")
|
||||
self.assertEqual(len(res["data"]["items"]), 2)
|
||||
self.assertEqual(res["data"]["items"][0]["title"], "Fleet sync loop")
|
||||
|
||||
|
||||
class TestBoxWorkCognitiveIntegration(unittest.TestCase):
|
||||
|
||||
@patch("box_work.acp.get_passive_cognitive_state")
|
||||
def test_cognitive_lock_blocks_start(self, mock_cog):
|
||||
mock_cog.return_value = {
|
||||
"node": "pip",
|
||||
"status": "THINKING",
|
||||
"cognitive_lock": True,
|
||||
"lock_reason": "Stop button active"
|
||||
}
|
||||
args = MagicMock()
|
||||
args.title = "New build"
|
||||
args.agent = "pip"
|
||||
args.goal = None
|
||||
args.force = False
|
||||
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
box_work.cmd_start(args)
|
||||
self.assertEqual(cm.exception.code, 1)
|
||||
|
||||
@patch("box_work.acp.get_passive_cognitive_state")
|
||||
def test_cognitive_lock_blocks_assign(self, mock_cog):
|
||||
mock_cog.return_value = {
|
||||
"node": "646",
|
||||
"status": "INPUT_WAIT",
|
||||
"cognitive_lock": True,
|
||||
"lock_reason": "Parked input card"
|
||||
}
|
||||
args = MagicMock()
|
||||
args.issue = 218
|
||||
args.agent = "646"
|
||||
args.force = False
|
||||
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
box_work.cmd_assign(args)
|
||||
self.assertEqual(cm.exception.code, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user