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>
160 lines
6.8 KiB
Python
160 lines
6.8 KiB
Python
# __main__.py — CLI bridge for lib_py.agents facts and resolution (N4)
|
|
|
|
import os, sys, json, shlex
|
|
from lib_py.agents.registry import get_adapter, own_key, agent_of_row
|
|
|
|
# `purge auto` probes adapters in this order; opencode last because its
|
|
# artifact path is one shared DB that exists for every uuid.
|
|
PURGE_PROBE_ORDER = ('agy', 'claude', 'hermes', 'grok', 'codex', 'opencode')
|
|
|
|
|
|
def _resolve_purge_target(agent_hint: str, uuid: str, workspace: str):
|
|
"""(agent, adapter, ctx) whose on-disk artifact carries `uuid`, else None.
|
|
|
|
An explicit agent is trusted as long as its artifact exists. `auto` tries
|
|
each adapter's own verification first (revalidate mode: an orchestrator
|
|
conversation is not in any workspace cache), then the adapter's own
|
|
`has_artifacts` - which, for agy, also accepts orphaned sidecars, the
|
|
annotation or the presence lock left behind after the DB is gone.
|
|
"""
|
|
from lib_py.agents.base import DiscoveryContext
|
|
names = [agent_hint] if agent_hint and agent_hint != 'auto' else list(PURGE_PROBE_ORDER)
|
|
probes = []
|
|
for name in names:
|
|
adapter = get_adapter(name)
|
|
if not adapter:
|
|
continue
|
|
probes.append((name, adapter, DiscoveryContext(workspace, name, mode="revalidate")))
|
|
for name, adapter, ctx in probes:
|
|
try:
|
|
if adapter.verify_artifact(uuid, ctx):
|
|
return name, adapter, ctx
|
|
except Exception:
|
|
pass
|
|
for name, adapter, ctx in probes:
|
|
try:
|
|
if adapter.has_artifacts(uuid, ctx):
|
|
return name, adapter, ctx
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python -m lib_py.agents <facts|resolve|purge> [args...]", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
cmd = sys.argv[1]
|
|
if cmd == 'facts':
|
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
adapter = get_adapter(agent_name)
|
|
if not adapter:
|
|
print(f"ERROR: Unknown agent {agent_name!r}", file=sys.stderr)
|
|
sys.exit(1)
|
|
# Emit shell-eval friendly facts
|
|
q = shlex.quote
|
|
print(f"MAM_AGENT_NAME={q(adapter.name)}")
|
|
print(f"MAM_OWN_KEY={q(adapter.own_key)}")
|
|
print(f"MAM_INPUT_PROMPT={q(adapter.input_prompt or '')}")
|
|
print(f"MAM_INPUT_PLACEHOLDER={q(adapter.input_placeholder or '')}")
|
|
print(f"MAM_INPUT_RULE_PATTERN={q(adapter.input_rule_pattern or '')}")
|
|
print(f"MAM_INPUT_FRAME_CHARS={q(adapter.input_frame_chars)}")
|
|
print(f"MAM_INPUT_BLOCK_PREFIX={q(adapter.input_block_prefix or '')}")
|
|
print(f"MAM_INPUT_STOP_PATTERN={q(adapter.input_stop_pattern or '')}")
|
|
print(f"MAM_READY_TOKENS={q(adapter.ready_tokens)}")
|
|
print(f"MAM_STRONG_READY_TOKENS={q(adapter.strong_ready_tokens)}")
|
|
print(f"MAM_WEAK_READY_TOKENS={q(adapter.weak_ready_tokens)}")
|
|
print(f"MAM_MODAL_TOKENS={q(adapter.modal_tokens or '')}")
|
|
print(f"MAM_STARTUP_DIALOGS={q(json.dumps(adapter.startup_dialogs))}")
|
|
print(f"MAM_RESIDENT_CHILDREN={q(json.dumps(list(adapter.resident_children)))}")
|
|
print(f"MAM_AGENT_STATUS_RELIABLE={1 if adapter.agent_status_reliable else 0}")
|
|
print(f"MAM_EXIT_KEY={q(adapter.exit_key)}")
|
|
print(f"MAM_DELEGATE_AGENT_KEY={q(adapter.delegate_agent_key)}")
|
|
|
|
elif cmd == 'resolve':
|
|
name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
resolved = agent_of_row({}, session_name=name)
|
|
if resolved:
|
|
print(resolved)
|
|
sys.exit(0)
|
|
sys.exit(1)
|
|
|
|
elif cmd == 'input-region':
|
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
pane_text = sys.stdin.read()
|
|
from lib_py.agents.input_region import extract_input_region
|
|
try:
|
|
region = extract_input_region(pane_text, agent_name)
|
|
print(region)
|
|
sys.exit(0)
|
|
except Exception as ex:
|
|
print(f"ERROR: {ex}", file=sys.stderr)
|
|
sys.exit(5)
|
|
|
|
elif cmd == 'spawn-spec':
|
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
binary = sys.argv[3] if len(sys.argv) > 3 else agent_name
|
|
uuid = sys.argv[4] if len(sys.argv) > 4 else ''
|
|
use_wrapper = (sys.argv[5].lower() in ('1', 'true', 'yes')) if len(sys.argv) > 5 else False
|
|
adapter = get_adapter(agent_name)
|
|
if not adapter:
|
|
sys.exit(1)
|
|
print(adapter.spawn_spec(binary, uuid, use_wrapper=use_wrapper))
|
|
sys.exit(0)
|
|
|
|
elif cmd == 'resume-spec':
|
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
binary = sys.argv[3] if len(sys.argv) > 3 else agent_name
|
|
uuid = sys.argv[4] if len(sys.argv) > 4 else ''
|
|
workspace = sys.argv[5] if len(sys.argv) > 5 else ''
|
|
adapter = get_adapter(agent_name)
|
|
if not adapter:
|
|
sys.exit(1)
|
|
from lib_py.agents.base import DiscoveryContext
|
|
ctx = DiscoveryContext(workspace, agent_name) if workspace else None
|
|
mat = adapter.verify_artifact(uuid, ctx) if (ctx and uuid) else False
|
|
print(adapter.resume_spec(binary, uuid, materialized=mat))
|
|
sys.exit(0)
|
|
|
|
elif cmd == 'exit-key':
|
|
agent_name = sys.argv[2] if len(sys.argv) > 2 else ''
|
|
adapter = get_adapter(agent_name)
|
|
if adapter:
|
|
print(adapter.exit_key)
|
|
sys.exit(0)
|
|
print('/exit')
|
|
sys.exit(0)
|
|
|
|
elif cmd == 'purge':
|
|
# purge <agent|auto> <uuid> [workspace] (Issue #13)
|
|
# Deletes a conversation's on-disk artifacts through its adapter and
|
|
# prints one JSON object: {"agent", "purged", "residual", "in_use"}.
|
|
# Exit 0 purged; 4 refused, conversation still in use (nothing
|
|
# removed); 5 no adapter has an artifact for this uuid.
|
|
agent_hint = sys.argv[2] if len(sys.argv) > 2 else 'auto'
|
|
uuid = sys.argv[3] if len(sys.argv) > 3 else ''
|
|
workspace = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else os.getcwd()
|
|
if not uuid:
|
|
print("ERROR: purge requires <agent|auto> <uuid>", file=sys.stderr)
|
|
sys.exit(1)
|
|
target = _resolve_purge_target(agent_hint, uuid, workspace)
|
|
if not target:
|
|
print(json.dumps({"agent": None, "purged": [], "residual": [], "in_use": False}))
|
|
sys.exit(5)
|
|
name, adapter, ctx = target
|
|
if adapter.conversation_in_use(uuid, ctx):
|
|
print(json.dumps({"agent": name, "purged": [], "residual": [], "in_use": True}))
|
|
sys.exit(4)
|
|
purged = adapter.purge_artifacts(uuid, ctx)
|
|
residual = adapter.residual_artifacts(uuid, ctx)
|
|
print(json.dumps({"agent": name, "purged": purged, "residual": residual, "in_use": False}))
|
|
sys.exit(0)
|
|
|
|
else:
|
|
print(f"ERROR: Unknown CLI command {cmd!r}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|