hermes - 💡(How to fix) Fix feat(gateway): wire clarify tool end-to-end with inline keyboard buttons on Telegram [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The clarify tool currently returns "Clarify tool is not available in this execution context." for any agent running in the gateway (Telegram, Discord, Slack, etc.). gateway/run.py never passes clarify_callback= into the AIAgent(...) constructor at any of its 4 instantiation sites, so the schema-documented behavior ("ask the user a multiple-choice question") silently no-ops on every messaging platform.

This issue scopes wiring clarify end-to-end in the gateway, with inline keyboard button rendering on Telegram as the first concrete UX. Other adapters (Discord, Slack, WhatsApp, Signal, Matrix, BlueBubbles, etc.) get a numbered-text fallback via the base adapter so clarify works everywhere, with rich button UI as platform-specific upgrades on top.

Error Message

Today the agent calls clarify on Telegram, gets back an error string, and the user never sees the question. This is a silent broken feature in the gateway.

Root Cause

The tool schema actively encourages calling clarify:

  • "The task is ambiguous and you need the user to choose an approach"
  • "Post-task feedback ('How did that work out?')"
  • "Offer to save a skill or update memory"
  • "A decision has meaningful trade-offs the user should weigh in on"

Today the agent calls clarify on Telegram, gets back an error string, and the user never sees the question. This is a silent broken feature in the gateway.

Fix Action

Fixed

Code Example

@dataclass
class _ClarifyEntry:
    event: threading.Event
    response: Optional[str] = None

_clarify_queues: dict[str, _ClarifyEntry] = {}

def register_gateway_clarify(clarify_id: str) -> _ClarifyEntry: ...
def resolve_gateway_clarify(clarify_id: str, response: str) -> int: ...
def wait_for_clarify(clarify_id: str, timeout: float) -> Optional[str]: ...

---

async def send_clarify(
    self,
    chat_id: str,
    question: str,
    choices: Optional[list[str]],
    clarify_id: str,
    session_key: str,
    metadata: Optional[dict] = None,
) -> SendResult: ...

---

keyboard_rows = []
if choices:
    for i, c in enumerate(choices):
        label = f"{i+1}. {c[:60]}"  # Telegram button cap is 64 bytes
        keyboard_rows.append([InlineKeyboardButton(label, callback_data=f"cl:{clarify_id}:{i}")])
    keyboard_rows.append([InlineKeyboardButton("✏️ Other (type answer)", callback_data=f"cl:{clarify_id}:other")])
keyboard = InlineKeyboardMarkup(keyboard_rows) if keyboard_rows else None

await self._bot.send_message(chat_id=int(chat_id), text=f"❓ {question}", reply_markup=keyboard, ...)
self._clarify_state[clarify_id] = (session_key, "buttons" if choices else "open")

---

self._clarify_awaiting_text: dict[str, str] = {}  # session_key -> clarify_id
RAW_BUFFERClick to expand / collapse

Summary

The clarify tool currently returns "Clarify tool is not available in this execution context." for any agent running in the gateway (Telegram, Discord, Slack, etc.). gateway/run.py never passes clarify_callback= into the AIAgent(...) constructor at any of its 4 instantiation sites, so the schema-documented behavior ("ask the user a multiple-choice question") silently no-ops on every messaging platform.

This issue scopes wiring clarify end-to-end in the gateway, with inline keyboard button rendering on Telegram as the first concrete UX. Other adapters (Discord, Slack, WhatsApp, Signal, Matrix, BlueBubbles, etc.) get a numbered-text fallback via the base adapter so clarify works everywhere, with rich button UI as platform-specific upgrades on top.

Why this matters

The tool schema actively encourages calling clarify:

  • "The task is ambiguous and you need the user to choose an approach"
  • "Post-task feedback ('How did that work out?')"
  • "Offer to save a skill or update memory"
  • "A decision has meaningful trade-offs the user should weigh in on"

Today the agent calls clarify on Telegram, gets back an error string, and the user never sees the question. This is a silent broken feature in the gateway.

Scope

1. Block-and-wait primitive — tools/clarify_gateway.py

Mirror the shape of tools/approval.py's resolve_gateway_approval():

@dataclass
class _ClarifyEntry:
    event: threading.Event
    response: Optional[str] = None

_clarify_queues: dict[str, _ClarifyEntry] = {}

def register_gateway_clarify(clarify_id: str) -> _ClarifyEntry: ...
def resolve_gateway_clarify(clarify_id: str, response: str) -> int: ...
def wait_for_clarify(clarify_id: str, timeout: float) -> Optional[str]: ...

2. Wire clarify_callback in gateway/run.py

All 4 AIAgent(...) sites (_process_message_background, hygiene-agent, cron worker, goal-judge) get a clarify_callback= kwarg pointing at a method that:

  1. Generates a clarify_id (monotonic counter or short uuid)
  2. Calls the adapter's send_clarify(...) with chat_id, question, choices, clarify_id, session_key, metadata
  3. Blocks on wait_for_clarify(clarify_id, timeout=clarify_timeout)
  4. Returns the resolved string (or a [user did not respond within Xm] sentinel on timeout)

3. Adapter base class — gateway/platforms/base.py

New abstract method:

async def send_clarify(
    self,
    chat_id: str,
    question: str,
    choices: Optional[list[str]],
    clarify_id: str,
    session_key: str,
    metadata: Optional[dict] = None,
) -> SendResult: ...

Default impl: render as numbered text list ("1. Choice A / 2. Choice B / Reply with the number or 'other:<text>'"). Captures the next message in the session as the response. Falls through to the normal message handler if no clarify is pending.

4. Telegram adapter — concrete send_clarify

Mirror send_exec_approval's pattern. Pseudocode:

keyboard_rows = []
if choices:
    for i, c in enumerate(choices):
        label = f"{i+1}. {c[:60]}"  # Telegram button cap is 64 bytes
        keyboard_rows.append([InlineKeyboardButton(label, callback_data=f"cl:{clarify_id}:{i}")])
    keyboard_rows.append([InlineKeyboardButton("✏️ Other (type answer)", callback_data=f"cl:{clarify_id}:other")])
keyboard = InlineKeyboardMarkup(keyboard_rows) if keyboard_rows else None

await self._bot.send_message(chat_id=int(chat_id), text=f"❓ {question}", reply_markup=keyboard, ...)
self._clarify_state[clarify_id] = (session_key, "buttons" if choices else "open")

Plus a CallbackQueryHandler for the cl: prefix:

  • cl:<id>:N (number) → resolve_gateway_clarify(clarify_id, choices[N]), edit message to show selection
  • cl:<id>:other → enter text-capture mode (see #5)

5. "Other" path — text capture for next message

When user picks the "Other" button, OR when clarify was open-ended (no choices), the next text message in that session becomes the response.

self._clarify_awaiting_text: dict[str, str] = {}  # session_key -> clarify_id

In _handle_message(), before normal routing: if session_key in self._clarify_awaiting_text, pop the entry, call resolve_gateway_clarify(clarify_id, message.text), and return early. This is the same intercept-first-then-fallthrough shape /approve already uses.

6. Discord adapter — discord.ui.View buttons (follow-up, optional)

Same UX as Telegram, using discord.ui.View + discord.ui.Button. Can ship in this PR or as a follow-up.

7. Timeouts

CLI clarify blocks forever, but the gateway can't — a stuck agent thread holds the running-agent guard and prevents /stop from working cleanly.

  • Config key: gateway.clarify_timeout_seconds, default 600 (10 minutes)
  • On timeout: return "[user did not respond within Xm]" so the agent can decide what to do (apologize, default-choose, give up)
  • Surface a brief warning to the user when timeout fires (rare, but better than silent)

8. Tests

Minimum coverage:

  • test_clarify_button_resolves: button press → callback resolves → wait_for_clarify returns the chosen string
  • test_clarify_other_captures_next_message: user picks "Other", next message is captured as response, normal handler doesn't see it
  • test_clarify_open_ended_captures_next_message: clarify with no choices skips buttons and captures text directly
  • test_clarify_timeout_returns_sentinel: response never arrives, sentinel string returned
  • test_clarify_handler_intercept_does_not_break_normal_flow: messages outside an active clarify go through normal routing

9. Docs

  • website/docs/user-guide/messaging/telegram.md: short "Interactive prompts" subsection covering buttons + Other
  • Mention in website/docs/user-guide/configuration.md under gateway settings (timeout)

Effort estimate

~250–400 LOC across:

  • tools/clarify_gateway.py (new, ~80 LOC)
  • gateway/platforms/base.py (+abstract method, +default text impl, ~50 LOC)
  • gateway/platforms/telegram.py (+send_clarify, +callback handler, +text-capture intercept, ~120 LOC)
  • gateway/run.py (+callback factory, +wire at 4 AIAgent sites, ~50 LOC)
  • hermes_cli/config.py (+gateway.clarify_timeout_seconds default)
  • Tests + docs

Single PR. Adapter parity for Discord/Slack/etc. lands as text fallback in this PR; rich button UI on Discord can be a separate follow-up.

Out of scope

  • Group polls for collective approve/deny — discussed and rejected, not a real use case
  • Bot-to-bot / guest mode (Bot API 10.0) — needs PTB ≥23 (just released, untested with our adapter), separate work
  • Checklists for todo — Premium business-account-only, separate issue worth filing later
  • Time/date message entities — separate polish, low priority

Design question (resolved)

"Other" path: Option A — always include "Other (type your answer)" as a final button when choices is provided, mirroring CLI behavior and matching the documented schema contract. Open-ended clarify (no choices) skips buttons entirely and captures the next message directly.

Background

Investigated as part of a Telegram blog feature audit covering 9.7 → 10.0 (Jul 2025 – May 2026). Other Bot API capabilities considered and parked: bot-to-bot (10.0), guest mode (10.0), checklists (9.1, business-account-only), poll media (10.0), live photos (10.0), document scanner (client-side), member tags, login-with-Telegram, business accounts, suggested posts, gifts, blockchain gifts, Stars billing.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING