hermes - 💡(How to fix) Fix feat(plugins): expose chat_id (and sender metadata) on pre_llm_call / post_llm_call hooks [1 participants]

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…
GitHub stats
NousResearch/hermes-agent#18992Fetched 2026-05-03 04:53:02
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Participants
Timeline (top)
labeled ×5

Root Cause

Plugins that need to push outbound messages (replies, approval cards, scheduled deliveries) currently can't, because the inbound MessageEvent's chat_id is never propagated to the hook layer. pre_llm_call only carries sender_id + platform; post_llm_call doesn't even carry those.

Fix Action

Fix / Workaround

We've shipped a workaround in PAID v0.5: detect the receive_id format and bypass the adapter with a direct lark_oapi call using the inferred receive_id_type. It works, but it reaches into the adapter's _client (private attr) and depends on lark-oapi import paths. An upstream fix would let plugins stay above that abstraction.

Filed against v0.12.0. Cross-reference: PAID plugin's workaround lives at https://github.com/jimmyag2026-prog/paid-plugin/blob/main/paid/hermes_io.py in _send_lark_direct and _detect_lark_receive_id_type.

Code Example

_pre_results = _invoke_hook(
    "pre_llm_call",
    session_id=self.session_id,
    user_message=original_user_message,
    conversation_history=list(messages),
    is_first_turn=(not bool(conversation_history)),
    model=self.model,
    platform=getattr(self, "platform", None) or "",
    sender_id=getattr(self, "_user_id", None) or "",
    chat_id=getattr(self, "_chat_id", None) or "",   # ← new
)
RAW_BUFFERClick to expand / collapse

TL;DR

Plugins that need to push outbound messages (replies, approval cards, scheduled deliveries) currently can't, because the inbound MessageEvent's chat_id is never propagated to the hook layer. pre_llm_call only carries sender_id + platform; post_llm_call doesn't even carry those.

For Lark / Feishu specifically this is fatal: gateway/platforms/feishu.py adapter send() hard-codes receive_id_type=chat_id, so an outbound call built from a plugin's stored sender_id (a tenant user_id) gets rejected by Lark with [230001] invalid receive_id.

Concrete impact

I'm building PAID — a Hermes plugin that runs an approval workflow over inbound IM messages. The flow is:

  1. Junior DMs the bot.
  2. PAID's pre_llm_call classifies the message; if the action is request, PAID stores a pending approval and DMs an approval card to the owner (separate Lark session).
  3. Owner runs /paid-approve <id> in their own session.
  4. PAID needs to push the approved final answer back to the junior.

Step 4 is where it falls apart: PAID has the junior's sender_id (4ed67983 in our test) but no chat_id, and the feishu adapter's send() only accepts chat_ids. The whole approval loop is broken without it.

We've shipped a workaround in PAID v0.5: detect the receive_id format and bypass the adapter with a direct lark_oapi call using the inferred receive_id_type. It works, but it reaches into the adapter's _client (private attr) and depends on lark-oapi import paths. An upstream fix would let plugins stay above that abstraction.

Proposal

1. Add chat_id to pre_llm_call kwargs (highest impact, smallest change)

Around run_agent.py:10656 (v0.12.0):

_pre_results = _invoke_hook(
    "pre_llm_call",
    session_id=self.session_id,
    user_message=original_user_message,
    conversation_history=list(messages),
    is_first_turn=(not bool(conversation_history)),
    model=self.model,
    platform=getattr(self, "platform", None) or "",
    sender_id=getattr(self, "_user_id", None) or "",
    chat_id=getattr(self, "_chat_id", None) or "",   # ← new
)

Plugins can then store (sender_id → chat_id) mappings on first inbound and use chat_id directly for outbound, eliminating the hard-coded-chat_id constraint downstream.

2. Pass platform + sender_id to post_llm_call too

Today only session_id / assistant_response / model make it through. Plugins doing per-counterparty post-checks (PAID's Layer 4 PII / cross-cp leakage scan, for instance) currently have to maintain their own session_id→sender map at pre-time — workable but redundant when the gateway already knows.

3. (Bonus, optional) chat_type, msg_id, sender_display_name

Useful but not blocking. chat_type (private/group) lets plugins gate behaviour for group messages; msg_id enables threaded reply UX; sender_display_name saves a contact-API roundtrip.

Why this is well-scoped

  • Backward compatible: callbacks that don't accept the new kwarg keep working (Python **kwargs already in use).
  • No behaviour change unless a plugin actively reads the new fields.
  • Already implemented inside MessageEvent.source — this is just a propagation gap.

Happy to send a PR if you'd accept this shape. Want to confirm the kwarg names and which subset (just chat_id, vs. the full bundle in §3) before I write it.


Filed against v0.12.0. Cross-reference: PAID plugin's workaround lives at https://github.com/jimmyag2026-prog/paid-plugin/blob/main/paid/hermes_io.py in _send_lark_direct and _detect_lark_receive_id_type.

extent analysis

TL;DR

Adding chat_id to pre_llm_call kwargs is the most likely fix to enable plugins to push outbound messages.

Guidance

  • The issue arises from the lack of chat_id propagation to the hook layer, causing plugins to fail when sending outbound messages.
  • To verify the fix, check if the chat_id is correctly passed to the pre_llm_call hook and if plugins can successfully send outbound messages using this chat_id.
  • Implementing the proposed change in run_agent.py around line 10656 by adding chat_id=getattr(self, "_chat_id", None) or "" to the _invoke_hook call for pre_llm_call should resolve the issue.
  • Additionally, passing platform and sender_id to post_llm_call can help plugins perform per-counterparty post-checks without maintaining redundant session maps.

Example

_pre_results = _invoke_hook(
    "pre_llm_call",
    session_id=self.session_id,
    user_message=original_user_message,
    conversation_history=list(messages),
    is_first_turn=(not bool(conversation_history)),
    model=self.model,
    platform=getattr(self, "platform", None) or "",
    sender_id=getattr(self, "_user_id", None) or "",
    chat_id=getattr(self, "_chat_id", None) or "",   # ← new
)

Notes

The proposed fix is backward compatible and does not introduce behavior changes unless a plugin actively reads the new fields. However, it is essential to confirm the kwarg names and the subset of fields to be added before implementing the change.

Recommendation

Apply the proposed workaround by adding chat_id to pre_llm_call kwargs, as it directly addresses the issue and enables plugins to push outbound messages.

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

hermes - 💡(How to fix) Fix feat(plugins): expose chat_id (and sender metadata) on pre_llm_call / post_llm_call hooks [1 participants]