Files
multi-agent-mux/.agents/skills/lib_py/agents/adapters/grok.py
T
GodopuandClaude Opus 5.5 622c1f8a9d feat(issue-30): a stall watcher that only notifies
lib_py/stall_watch.py runs beside the wrapper's subscriber for a local herdr
job (started after delivery, stopped when the subscriber ends; MAM_STALL_WATCH=0
turns it off). It follows `herdr agent wait` transitions and publishes only
`progress` events through one publisher; it has no path that closes a job.

- blocked: `stalled:blocked <detection line>` once per episode, never answered.
- idle for MAM_STALL_IDLE_S (600) with no background descendants of the agent
  pid (pane process-info + /proc; adapter resident_children and the heartbeat
  timer excluded) and an empty input box: `stalled:idle` and one nudge.
- input box judged from adapter facts measured on herdr 0.9.1 (codex '›',
  grok frame '│', opencode '┃' block + stop pattern); the whole-pane fallback
  is gone. No facts: withheld + `stall_watch:input_check_unavailable` once;
  never empty for 3 x S: `stall_watch:input_never_empty` once.
- herdr failures are not "no event": warning + `stall_watch:herdr_unavailable:
  <code>` (or agent_not_found), backoff, rc 2 after MAM_STALL_HERDR_FAIL_S
  (120); the wrapper prints the watcher's last lines.
- remote and human jobs are skipped with one log line.
- agent_status_reliable is an adapter field; false turns idle judgement off.
- the loop runner relays stalled:/stall_watch: notices once each.
- scoped_env drops a pane-inherited workspace scope for name lookups (#34 rule).

The module sits at the top of lib_py: delegation and loop may not import the
agent adapters (their sibling-import rules). Idle pane captures of the five
CLIs are fixtures; the mock herdr answers process-info in the real shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:43:21 +00:00

141 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os, json, glob, shutil
from typing import Optional, Any
from urllib.parse import quote
from lib_py.agents.base import BaseAgentAdapter, DiscoveryContext
from lib_py.verify_session import workspace_key
class GrokAgentAdapter(BaseAgentAdapter):
@property
def name(self) -> str:
return 'grok'
@property
def own_key(self) -> str:
return 'grok_session_id_own'
@property
def ready_tokens(self) -> str:
return 'Grok|xAI|Assistant|❯|>>>'
@property
def exit_key(self) -> str:
return '/exit'
@property
def delegate_agent_key(self) -> str:
return 'grok-build'
@property
def startup_dialogs(self) -> list:
# #24: "Do you trust the contents of this directory?" answered with y.
return [
('Do you trust the contents of this directory', ['y'], 'trust'),
]
@property
def identity_cache_fields(self) -> tuple:
return ('session_id', 'session_jsonl')
@property
def input_prompt(self) -> str:
return '❯'
@property
def input_placeholder(self) -> str:
return ''
@property
def input_rule_pattern(self) -> str:
return '─{10,}'
@property
def input_frame_chars(self) -> str:
# The input box is drawn as ╭─╮ / │ ❯ │ / ╰─╯ (herdr 0.9.1 capture).
return '│'
def _ws_dir(self, ctx: DiscoveryContext) -> str:
return quote(os.path.realpath(ctx.cwd), safe='')
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
return f"{ctx.home_dir}/.grok/sessions/{self._ws_dir(ctx)}/{uuid}/chat_history.jsonl"
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
path = self.artifact_path(uuid, ctx)
if not os.path.exists(path):
return False
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
return False
try:
valid_session = False
found_cwd = None
with open(path) as f:
for _ in range(50):
line = f.readline()
if not line:
break
line = line.strip()
if not line:
continue
try:
payload = json.loads(line)
if payload.get("session_id") == uuid or payload.get("sessionId") == uuid:
valid_session = True
if payload.get("cwd"):
found_cwd = payload.get("cwd")
break
except Exception:
pass
if not valid_session and found_cwd is None:
# Directory path itself encodes the workspace and session UUID
valid_session = os.path.isdir(os.path.dirname(path))
if not valid_session:
return False
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
return False
except Exception:
return False
return True
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
purged = []
sess_dir = os.path.dirname(self.artifact_path(uuid, ctx))
if os.path.isdir(sess_dir):
shutil.rmtree(sess_dir, ignore_errors=True)
purged.append(sess_dir)
elif os.path.exists(self.artifact_path(uuid, ctx)):
os.remove(self.artifact_path(uuid, ctx))
purged.append(self.artifact_path(uuid, ctx))
return purged
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
if session_uuid:
return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions"
return f"{binary} --permission-mode bypassPermissions"
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
if materialized and session_uuid:
return f"{binary} --resume {session_uuid} --permission-mode bypassPermissions"
if session_uuid:
return f"{binary} --session-id {session_uuid} --permission-mode bypassPermissions"
return f"{binary} --permission-mode bypassPermissions"
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
if os.environ.get("XAI_API_KEY"):
return True
home = os.environ.get("HOME_DIR") or os.environ.get("HOME") or os.path.expanduser("~")
return os.path.exists(f"{home}/.grok/auth.json")
def discover(self, ctx: DiscoveryContext) -> list:
root = f"{ctx.home_dir}/.grok/sessions/{self._ws_dir(ctx)}"
if not os.path.isdir(root):
return []
entries = [d for d in glob.glob(f"{root}/*") if os.path.isdir(d)]
entries.sort(key=os.path.getmtime, reverse=True)
candidates = []
for d in entries:
cand = os.path.basename(d)
if cand and self.verify_artifact(cand, ctx):
candidates.append(cand)
return candidates