mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/tui-audit-followup
# Conflicts: # ui-tui/src/components/markdown.tsx # ui-tui/src/types/hermes-ink.d.ts
This commit is contained in:
commit
93b4080b78
71 changed files with 3243 additions and 42 deletions
|
|
@ -1954,6 +1954,13 @@ _TUI_EXTRA: list[tuple[str, str, str]] = [
|
|||
("/logs", "Show recent gateway log lines", "TUI"),
|
||||
]
|
||||
|
||||
# Commands that queue messages onto _pending_input in the CLI.
|
||||
# In the TUI the slash worker subprocess has no reader for that queue,
|
||||
# so slash.exec rejects them → TUI falls through to command.dispatch.
|
||||
_PENDING_INPUT_COMMANDS: frozenset[str] = frozenset({
|
||||
"retry", "queue", "q", "steer", "plan",
|
||||
})
|
||||
|
||||
|
||||
@method("commands.catalog")
|
||||
def _(rid, params: dict) -> dict:
|
||||
|
|
@ -2149,6 +2156,75 @@ def _(rid, params: dict) -> dict:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# ── Commands that queue messages onto _pending_input in the CLI ───
|
||||
# In the TUI the slash worker subprocess has no reader for that queue,
|
||||
# so we handle them here and return a structured payload.
|
||||
|
||||
if name in ("queue", "q"):
|
||||
if not arg:
|
||||
return _err(rid, 4004, "usage: /queue <prompt>")
|
||||
return _ok(rid, {"type": "send", "message": arg})
|
||||
|
||||
if name == "retry":
|
||||
if not session:
|
||||
return _err(rid, 4001, "no active session to retry")
|
||||
history = session.get("history", [])
|
||||
if not history:
|
||||
return _err(rid, 4018, "no previous user message to retry")
|
||||
# Walk backwards to find the last user message
|
||||
last_user_idx = None
|
||||
for i in range(len(history) - 1, -1, -1):
|
||||
if history[i].get("role") == "user":
|
||||
last_user_idx = i
|
||||
break
|
||||
if last_user_idx is None:
|
||||
return _err(rid, 4018, "no previous user message to retry")
|
||||
content = history[last_user_idx].get("content", "")
|
||||
if isinstance(content, list):
|
||||
content = " ".join(
|
||||
p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text"
|
||||
)
|
||||
if not content:
|
||||
return _err(rid, 4018, "last user message is empty")
|
||||
# Truncate history: remove everything from the last user message onward
|
||||
# (mirrors CLI retry_last() which strips the failed exchange)
|
||||
with session["history_lock"]:
|
||||
session["history"] = history[:last_user_idx]
|
||||
session["history_version"] = int(session.get("history_version", 0)) + 1
|
||||
return _ok(rid, {"type": "send", "message": content})
|
||||
|
||||
if name == "steer":
|
||||
if not arg:
|
||||
return _err(rid, 4004, "usage: /steer <prompt>")
|
||||
agent = session.get("agent") if session else None
|
||||
if agent and hasattr(agent, "steer"):
|
||||
try:
|
||||
accepted = agent.steer(arg)
|
||||
if accepted:
|
||||
return _ok(rid, {"type": "exec", "output": f"⏩ Steer queued — arrives after the next tool call: {arg[:80]}{'...' if len(arg) > 80 else ''}"})
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback: no active run, treat as next-turn message
|
||||
return _ok(rid, {"type": "send", "message": arg})
|
||||
|
||||
if name == "plan":
|
||||
try:
|
||||
from agent.skill_commands import build_skill_invocation_message as _bsim, build_plan_path
|
||||
user_instruction = arg or ""
|
||||
plan_path = build_plan_path(user_instruction)
|
||||
msg = _bsim(
|
||||
"/plan", user_instruction,
|
||||
task_id=session.get("session_key", "") if session else "",
|
||||
runtime_note=(
|
||||
"Save the markdown plan with write_file to this exact relative path "
|
||||
f"inside the active workspace/backend cwd: {plan_path}"
|
||||
),
|
||||
)
|
||||
if msg:
|
||||
return _ok(rid, {"type": "send", "message": msg})
|
||||
except Exception as e:
|
||||
return _err(rid, 5030, f"plan skill failed: {e}")
|
||||
|
||||
return _err(rid, 4018, f"not a quick/plugin/skill command: {name}")
|
||||
|
||||
|
||||
|
|
@ -2365,6 +2441,26 @@ def _(rid, params: dict) -> dict:
|
|||
if not cmd:
|
||||
return _err(rid, 4004, "empty command")
|
||||
|
||||
# Skill slash commands and _pending_input commands must NOT go through the
|
||||
# slash worker — see _PENDING_INPUT_COMMANDS definition above.
|
||||
# (/browser connect/disconnect also uses _pending_input for context
|
||||
# notes, but the actual browser operations need the slash worker's
|
||||
# env-var side effects, so they stay in slash.exec — only the context
|
||||
# note to the model is lost, which is low-severity.)
|
||||
_cmd_parts = cmd.split() if not cmd.startswith("/") else cmd.lstrip("/").split()
|
||||
_cmd_base = _cmd_parts[0] if _cmd_parts else ""
|
||||
|
||||
if _cmd_base in _PENDING_INPUT_COMMANDS:
|
||||
return _err(rid, 4018, f"pending-input command: use command.dispatch for /{_cmd_base}")
|
||||
|
||||
try:
|
||||
from agent.skill_commands import get_skill_commands
|
||||
_cmd_key = f"/{_cmd_base}"
|
||||
if _cmd_key in get_skill_commands():
|
||||
return _err(rid, 4018, f"skill command: use command.dispatch for {_cmd_key}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
worker = session.get("slash_worker")
|
||||
if not worker:
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue