hermes - ✅(Solved) Fix [Feature]: Add plugin hook for ephemeral API-message transformation (transform_api_message) [1 pull requests, 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#20307Fetched 2026-05-06 06:37:26
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
labeled ×4cross-referenced ×1

Fix Action

Fix / Workaround

  1. Monkey-patch run_conversation(): A plugin could monkey-patch the api_messages loop at import time. This works but is brittle across Hermes version upgrades and bypasses the hook system entirely.

PR fix notes

PR #20504: feat(plugins): add transform_api_message hook

Description (problem / solution / changelog)

Summary

Fixes #20307.

Adds a transform_api_message plugin hook at the API-message copy boundary so plugins can make request-scoped message changes without mutating the canonical messages list that is persisted to session storage.

Changes

  • Registers transform_api_message as a valid plugin hook.
  • Invokes it for each copied API message after Hermes' existing ephemeral mutations/internal-field stripping and before appending to api_messages.
  • Adds synthetic payload coverage for hermes hooks test.
  • Adds regression tests proving hook mutations affect only api_msg, not the original message, and hook failures leave the API message unchanged.

Overlap Check

  • Searched open PRs for 20307, transform_api_message, and ephemeral API-message transformation.
  • No open PR overlap found.

Verification

  • scripts/run_tests.sh tests/run_agent/test_transform_api_message_hook.py tests/hermes_cli/test_plugins.py -k 'transform_api_message or valid_hooks_include_request_scoped_api_hooks' -> 3 passed
  • scripts/run_tests.sh tests/hermes_cli/test_hooks_cli.py tests/run_agent/test_transform_api_message_hook.py tests/hermes_cli/test_plugins.py -k 'transform_api_message or valid_hooks_include_request_scoped_api_hooks or hooks' -> 27 passed
  • git diff --check -> clean

Changed files

  • hermes_cli/hooks.py (modified, +8/-0)
  • hermes_cli/plugins.py (modified, +1/-0)
  • run_agent.py (modified, +25/-1)
  • tests/hermes_cli/test_plugins.py (modified, +1/-0)
  • tests/run_agent/test_transform_api_message_hook.py (added, +55/-0)

Code Example

# Fired once per message during api_messages construction
ctx.register_hook("transform_api_message", callback)

# Callback receives:
def callback(
    hook_name: str,
    msg: dict,           # original message from `messages` (READ-ONLY)
    api_msg: dict,       # shallow copy being built for API (MUTABLE)
    idx: int,            # index in messages list
    session_id: str,
    model: str,
    **kwargs,
) -> None:
    ...
RAW_BUFFERClick to expand / collapse

Problem or Use Case

Plugin authors who need to transform messages before they reach the LLM API currently have no clean hook point.

The existing hooks have limitations:

  • transform_tool_result fires inside handle_function_call() when a tool result is first returned. It replaces the result in the in-memory messages list, which means the transformation persists to the SQLite session DB. This is fine for permanent transformations, but not for ephemeral ones (e.g., trimming verbose content for the current API call while preserving the full result for session resumption).

  • pre_llm_call fires once per turn before the tool-calling loop. It receives conversation_history but: (a) it fires before the current turn's tool calls have executed, so it can't transform current-turn tool results; (b) its return value is interpreted as "context to inject into the user message", not as message transformations; and (c) if a callback mutates the list in-place, those mutations affect both the persisted messages list and api_messages.

  • pre_api_request is purely observational — it receives metadata (message_count, tool_count, etc.) but not the actual messages.

The api_messages construction loop in run_conversation() (lines ~10991–11034) already implements the right pattern: it makes shallow copies of messages and applies ephemeral mutations (context injection, field stripping) only to the copies. But there is no hook there for plugins to participate.

Use cases that need this:

  • Ephemeral content truncation/summarization (trade verbosity for context window)
  • PII redaction before sending to third-party APIs (while retaining in local session)
  • Injecting ephemeral hints or structured annotations into tool results for the current call
  • Transforming provider-specific message formats without affecting the canonical conversation

Proposed Solution

Add a new plugin hook — tentatively transform_api_message — that fires for each message as it's being converted from messages to api_messages.

Hook signature

# Fired once per message during api_messages construction
ctx.register_hook("transform_api_message", callback)

# Callback receives:
def callback(
    hook_name: str,
    msg: dict,           # original message from `messages` (READ-ONLY)
    api_msg: dict,       # shallow copy being built for API (MUTABLE)
    idx: int,            # index in messages list
    session_id: str,
    model: str,
    **kwargs,
) -> None:
    ...

Behavior

  • The hook is called for every message in the conversation, once per API call iteration.
  • The callback receives both the original msg (for context) and the mutable api_msg (the copy being sent to the LLM).
  • Modifications to api_msg are ephemeral — they affect only the current API request, not the persisted messages list.
  • Return value is ignored (the hook mutates api_msg in place).
  • Errors in any callback are caught and logged; the unmodified api_msg is used as fallback.

Integration point

The hook would be inserted in the api_messages construction loop in run_conversation(), after the shallow copy is made and existing ephemeral mutations are applied, but before api_msg is appended to the outgoing list (i.e., between lines ~11034 and 11035 in the current api_messages loop).

Config

No additional config needed. Like other plugin hooks, the hook is available whenever the plugin is loaded.

Alternatives Considered

  1. Abuse pre_llm_call with in-place mutation + restore in post_api_request: Requires the plugin to mutate messages in-place, then restore original content before persistence. This is fragile — the persistence path has multiple exit points, and any missed restore corrupts the session DB.

  2. Abuse transform_tool_result with side-channel storage: Store full results externally and always return trimmed. On session resume, a pre_llm_call hook (or session-load hook) would need to restore originals. This is complex, fragile, and couples the trimming logic to session load timing.

  3. Monkey-patch run_conversation(): A plugin could monkey-patch the api_messages loop at import time. This works but is brittle across Hermes version upgrades and bypasses the hook system entirely.

The proposed hook is the natural extension of the existing api_messages copy-on-write pattern — it gives plugins the same ephemeral mutation capability that Hermes internals already use for context injection and field stripping.

Feature Type

Developer experience (tests, docs, CI)

Scope

Small (single file, < 50 lines)

Contribution

  • I'd like to implement this myself and submit a PR

extent analysis

TL;DR

Add a new plugin hook, transform_api_message, to allow plugins to transform messages before they are sent to the LLM API without affecting the persisted conversation history.

Guidance

  • Implement the proposed transform_api_message hook in the api_messages construction loop in run_conversation(), allowing plugins to mutate the api_msg copy without affecting the original messages list.
  • Ensure the hook is called for every message in the conversation, once per API call iteration, and that modifications to api_msg are ephemeral.
  • Handle errors in callbacks by logging the error and using the unmodified api_msg as a fallback.
  • Consider adding documentation and tests for the new hook to ensure its correct usage and behavior.

Example

# Example callback function for the transform_api_message hook
def transform_api_message_callback(
    hook_name: str,
    msg: dict,           # original message from `messages` (READ-ONLY)
    api_msg: dict,       # shallow copy being built for API (MUTABLE)
    idx: int,            # index in messages list
    session_id: str,
    model: str,
    **kwargs,
) -> None:
    # Trim verbose content, for example
    if len(api_msg["content"]) > 1000:
        api_msg["content"] = api_msg["content"][:1000] + "..."

Notes

The proposed solution is a natural extension of the existing api_messages copy-on-write pattern and provides a clean and flexible way for plugins to transform messages without affecting the persisted conversation history. However, the implementation details and error handling should be carefully considered to ensure the hook is robust and reliable.

Recommendation

Apply the proposed solution by adding the transform_api_message hook to the api_messages construction loop, as it provides a clean and flexible way for plugins to transform messages without affecting the persisted conversation

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 - ✅(Solved) Fix [Feature]: Add plugin hook for ephemeral API-message transformation (transform_api_message) [1 pull requests, 1 participants]