hermes - ✅(Solved) Fix [RFC] Allow per-AIAgent tool injection via extra_tools= (generalize the memory_manager pattern) [2 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#13344Fetched 2026-04-22 08:06:56
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×4

Fix Action

Fix / Workaround

Dispatch: route to per-agent handler before registry fallback

elif self._memory_manager and self._memory_manager.has_tool(function_name): return self._memory_manager.handle_tool_call(function_name, function_args)


- Schemas merge into `self.tools` under the same `{"type": "function", ...}`
  envelope as registry tools, so the model sees them uniformly.
- Handlers are indexed in a per-instance `self._extra_tool_handlers` dict.
- `_invoke_tool` dispatches them **after** the hard-coded built-ins
  (`todo` / `memory` / `clarify` / `delegate_task` / `session_search`)
  and **before** the registry fallback. This means extra_tools can
  augment an agent but cannot shadow critical built-ins.
- Name collisions with entries already in `self.tools` are logged and
  skipped.

1. **Direction**: is generalizing the per-agent tool-injection pattern
   the right move? If not, what's the preferred alternative for "tool
   real enough to show the model but should not live in the global
   registry"?
2. **Naming**: `extra_tools`, `agent_tools`, `instance_tools`,
   `scoped_tools`? Happy to follow whatever convention fits hermes's
   style.
3. **Dispatch order**: PR places extra_tools *after* hard-coded
   built-ins (cannot shadow `todo` / `memory` / etc.) and *before* the
   registry fallback. I picked "cannot shadow built-ins" as the safety
   default. If the convention is "caller knows best", reversing this
   is a one-line change.
4. **Migration**: assuming the answer to (1) is yes, should the
   existing memory-provider injection migrate onto `extra_tools=` in
   the same PR, in a follow-up, or stay separate indefinitely? I have
   a mild preference for "follow-up so this PR stays small".

PR fix notes

PR #13315: [RFC] feat(agent): allow per-AIAgent tool injection via extra_tools=

Description (problem / solution / changelog)

Motivation

Today every tool an agent can reach must be registered in the global tools.registry at import time. That fits cross-cutting capabilities (memory, session_search, todo, send_message, …) but is a poor fit for two other kinds of tool:

  1. Domain-specific — only meaningful inside one platform or scenario (e.g. a Feishu doc reader, RL training, a Home Assistant entity getter). Today such tools are visible to any agent that opts into their toolset, even on unrelated channels.
  2. Instance-bound — the handler needs to close over a runtime value owned by the caller (API client, per-event whitelist, auth context). Today those handlers resort to thread-locals or module singletons, with the familiar correctness concerns around asyncio.to_thread and multi-account fan-out.

The pattern already exists — just not generalized

Hermes already implements per-agent tool injection for exactly one consumer: the memory provider.

run_agent.py lines ~1408-1422:

if self._memory_manager and self.tools is not None:
    _existing = {t.get("function", {}).get("name") for t in self.tools}
    for _schema in self._memory_manager.get_all_tool_schemas():
        _tname = _schema.get("name", "")
        if _tname in _existing:
            continue
        self.tools.append({"type": "function", "function": _schema})
        self.valid_tool_names.add(_tname)

And in _invoke_tool (~line 7998):

elif self._memory_manager and self._memory_manager.has_tool(function_name):
    return self._memory_manager.handle_tool_call(function_name, function_args)

That is conceptually extra_tools — schemas merged into self.tools plus a dispatch branch that checks per-agent handler storage before falling through to the global registry. It's just hard-wired for memory.

Proposal

Promote the pattern to a generic AIAgent.__init__ parameter:

AIAgent(
    ...,
    extra_tools=[{"schema": <openai-fn-schema>, "handler": <callable>}, ...],
)

Behavior (see the commit for the ~25-line implementation):

  • Schemas merge into self.tools under the same {"type": "function", ...} envelope as registry tools, so the model sees them uniformly.
  • Handlers are indexed in a per-instance self._extra_tool_handlers dict and dispatched by _invoke_tool after the hard-coded agent-level built-ins (todo / memory / clarify / delegate_task / session_search) and before the registry fallback. Order means extra_tools can augment an agent but cannot shadow critical built-ins.
  • Name collisions with entries already in self.tools (registry or earlier extra_tools) are logged and skipped.

Concrete use case

PR #13045 reworks the Feishu document-comment integration. Two findings there would have been unnecessary with extra_tools:

  • feishu_doc_read and the drive-comment tools had to live in the global registry, even though they are only meaningful inside the comment handler. They were deleted and replaced with an ad-hoc two-pass <NEED_DOC_READ> sentinel protocol on top of plaintext responses. That protocol has been harder to secure (three injection variants found so far) than a proper function-call tool would be.
  • The lark client had to be injected via threading.local because there was no other way to pass an instance-bound dependency to a registry-level handler. This raised correctness concerns around asyncio.to_thread and multi-account paths.

Other likely consumers:

  • rl_* — only meaningful when an RL training run is active.
  • homeassistant/* — only meaningful when HASS_TOKEN is configured and a Home Assistant server is reachable.
  • Future external plugins via hermes_cli/plugins: today PluginContext.register_tool calls registry.register, making the plugin's tools visible to every agent with no hook for per-agent scoping.

Discussion points

  1. Is the general direction acceptable? If not, what's the preferred alternative for "tool real enough to show the model but should not live in the global registry"?
  2. Bikeshed: parameter name — extra_tools, agent_tools, instance_tools, scoped_tools? Happy to follow whatever convention fits hermes's style.
  3. Dispatch order: current placement is after hard-coded built-ins, before registry fallback. I chose "cannot shadow built-ins" as the default safety posture; open to reversing it if the convention is "caller knows best".

Test plan

  • 11 new unit tests in tests/run_agent/test_extra_tools.py covering construction (None / empty / single / multiple), shadow prevention (registry collision, built-in collision), dispatch (handler invoked for its name, unknown names fall through to registry, built-in dispatch order wins over extra_tools), and schema-envelope symmetry with registry tools.
  • Existing tests/run_agent/test_run_agent.py (275 tests) passes unchanged.

Changed files

  • run_agent.py (modified, +55/-2)
  • tests/run_agent/test_extra_tools.py (added, +271/-0)

PR #13045: refactor(feishu-comment): use SessionStore, sentinel doc-read, redacted logs

Description (problem / solution / changelog)

Summary

Reworks the feishu document-comment integration (#11898) to align with hermes's core session architecture and close review-flagged concerns around session bloat, tool scoping, and log leakage.

Session management

  • Replace the in-memory _session_cache dict (1h hard TTL, no persistence) with hermes's generic SessionStore pipeline (SessionSource + SessionDB). Sessions now persist through gateway restart and auto-reset on daily/idle policy, matching IM.
  • Local comments key on comment_id (per-thread isolation).
  • Whole-doc comments collapse onto a __whole_doc__ sentinel thread_id so all whole-document comments on the same doc share one document-level session.
  • Persist only the user's actual comment text + optional quote anchor, not the rendered prompt (~50x smaller per user row).

Tool scoping

  • Delete tools/feishu_doc_tool.py and tools/feishu_drive_tool.py. These are feishu-specific document/comment operations, not cross-cutting agent capabilities — yet they lived in the global tool registry alongside core tools like memory, session_search, and send_message. The global registry should host tools any agent on any platform legitimately uses; feishu-scoped surfaces belong inside the feishu handler, not next to memory/session_search. Pulling these out closes that scope mismatch.
  • Document content now flows through a two-pass <NEED_DOC_READ> sentinel protocol owned by the comment handler: the agent lists the tokens it needs, business code fetches them against a whitelist (source doc + comment-referenced docs, docx only), and the agent responds on the second turn.
  • Drop the thread-local client injection; the lark client is built from config at call time.

Log hardening

  • Strip user comment text, quote text, agent response text, and full prompts from all log statements. ~/.hermes/logs/agent.log now stores only identifiers, lengths, and status codes — closes the multi-operator leak risk where operators sharing a hermes instance could read each other's document content through log files.

Adapter wiring

  • handle_drive_comment_event now takes the FeishuAdapter instance instead of the bare client so it can reach the gateway-injected SessionStore. The change stays fully off the IM message path.

Test plan

  • New unit tests for session-source construction, sentinel parsing, whitelist enforcement, doc-content truncation, history persistence, compact user-turn rendering, and error-path degradation
  • Full tests/gateway/ regression passes
  • Live smoke test:
    • Local comment thread creates a session keyed by comment_id
    • Whole-doc comments share one __whole_doc__ session across users
    • History loads (history=N grows turn-by-turn)
    • <NEED_DOC_READ> sentinel triggers parallel fetch of source + wiki-resolved referenced docs
    • Long replies auto-chunk to fit feishu's per-comment length limit
    • SessionDB rows show compact [Quoted] … payloads, not full prompts

Changed files

  • gateway/platforms/feishu.py (modified, +10/-2)
  • gateway/platforms/feishu_comment.py (modified, +945/-144)
  • gateway/platforms/feishu_comment_rules.py (modified, +15/-0)
  • tests/gateway/test_feishu_comment.py (modified, +900/-11)
  • tests/gateway/test_feishu_comment_rules.py (modified, +18/-0)
  • tests/tools/test_feishu_tools.py (removed, +0/-62)
  • tests/tools/test_registry.py (modified, +0/-2)
  • tools/feishu_doc_tool.py (removed, +0/-131)
  • tools/feishu_drive_tool.py (removed, +0/-429)
  • toolsets.py (modified, +0/-16)
  • website/docs/reference/tools-reference.md (modified, +0/-19)
  • website/docs/reference/toolsets-reference.md (modified, +1/-3)
  • website/docs/user-guide/messaging/feishu.md (modified, +1/-1)

Code Example

# Construction: merge schemas into self.tools
if self._memory_manager and self.tools is not None:
    for _schema in self._memory_manager.get_all_tool_schemas():
        if _schema.get("name") in _existing:
            continue
        self.tools.append({"type": "function", "function": _schema})
        self.valid_tool_names.add(_schema.get("name"))

# Dispatch: route to per-agent handler before registry fallback
elif self._memory_manager and self._memory_manager.has_tool(function_name):
    return self._memory_manager.handle_tool_call(function_name, function_args)

---

AIAgent(
    ...,
    extra_tools=[{"schema": <openai-fn-schema>, "handler": <callable>}, ...],
)
RAW_BUFFERClick to expand / collapse

Companion PR with reference implementation: #13315 — feat(agent): allow per-AIAgent tool injection via extra_tools=

Opening this issue separately from the PR to keep the design discussion distinct from the code review. Happy to consolidate either way maintainers prefer.

Problem

Today every tool an agent can reach must be registered in the global tools.registry at import time. That fits cross-cutting capabilities (memory, session_search, todo, send_message, …) but is a poor fit for two other kinds of tool:

  1. Domain-specific — only meaningful inside one platform or scenario (a Feishu doc reader, RL training, a Home Assistant entity getter). Today such tools are visible to any agent that opts into their toolset, even on unrelated channels. There's no way to say "this tool exists, but only for this agent instance".
  2. Instance-bound — the handler needs to close over a runtime value owned by the caller (API client, per-event whitelist, auth context). Today such handlers resort to thread-locals or module singletons, with the familiar correctness concerns around asyncio.to_thread and multi-account fan-out.

The pattern already exists for one case

Hermes already implements per-agent tool injection — but only for the memory provider. See run_agent.py lines ~1408-1422 and ~7998:

# Construction: merge schemas into self.tools
if self._memory_manager and self.tools is not None:
    for _schema in self._memory_manager.get_all_tool_schemas():
        if _schema.get("name") in _existing:
            continue
        self.tools.append({"type": "function", "function": _schema})
        self.valid_tool_names.add(_schema.get("name"))

# Dispatch: route to per-agent handler before registry fallback
elif self._memory_manager and self._memory_manager.has_tool(function_name):
    return self._memory_manager.handle_tool_call(function_name, function_args)

That is conceptually extra_tools — the implementation is just hard-wired for memory.

Proposal

Promote the pattern to a generic AIAgent.__init__ parameter:

AIAgent(
    ...,
    extra_tools=[{"schema": <openai-fn-schema>, "handler": <callable>}, ...],
)
  • Schemas merge into self.tools under the same {"type": "function", ...} envelope as registry tools, so the model sees them uniformly.
  • Handlers are indexed in a per-instance self._extra_tool_handlers dict.
  • _invoke_tool dispatches them after the hard-coded built-ins (todo / memory / clarify / delegate_task / session_search) and before the registry fallback. This means extra_tools can augment an agent but cannot shadow critical built-ins.
  • Name collisions with entries already in self.tools are logged and skipped.

The shape intentionally mirrors the existing memory-manager injection so the two can be unified in the future.

The full implementation is in #13315 (~25 lines + 11 unit tests).

Concrete use case

#13045 reworks the Feishu document-comment integration. Two of its findings would have been unnecessary with extra_tools=:

  • feishu_doc_read and the drive-comment tools had to live in the global registry, even though they are useless outside the comment handler. They were eventually deleted and replaced with an ad-hoc two-pass <NEED_DOC_READ> sentinel protocol layered on top of the agent's plaintext response. That protocol has been harder to secure (three injection variants found so far) than a proper function-call tool would be.
  • The lark client had to be injected via threading.local, with follow-on correctness concerns around asyncio.to_thread and multi-account paths.

Other likely consumers:

  • rl_* — only meaningful when an RL training run is active.
  • homeassistant/* — only meaningful when HASS_TOKEN is configured.
  • Future external plugins via hermes_cli/plugins. Today PluginContext.register_tool calls registry.register, exposing the plugin's tools to every agent with no per-agent scoping hook.

What I'd like to discuss

  1. Direction: is generalizing the per-agent tool-injection pattern the right move? If not, what's the preferred alternative for "tool real enough to show the model but should not live in the global registry"?
  2. Naming: extra_tools, agent_tools, instance_tools, scoped_tools? Happy to follow whatever convention fits hermes's style.
  3. Dispatch order: PR places extra_tools after hard-coded built-ins (cannot shadow todo / memory / etc.) and before the registry fallback. I picked "cannot shadow built-ins" as the safety default. If the convention is "caller knows best", reversing this is a one-line change.
  4. Migration: assuming the answer to (1) is yes, should the existing memory-provider injection migrate onto extra_tools= in the same PR, in a follow-up, or stay separate indefinitely? I have a mild preference for "follow-up so this PR stays small".

I'd appreciate a maintainer steer before doing more downstream work (e.g. simplifying #13045 to use this).

extent analysis

TL;DR

The proposed solution involves introducing an extra_tools parameter to the AIAgent constructor to allow per-agent tool injection, which can help resolve the issue of domain-specific and instance-bound tools.

Guidance

  • Review the proposed implementation in PR #13315 and consider the trade-offs of introducing extra_tools as a generic parameter.
  • Discuss the naming convention for the new parameter, such as extra_tools, agent_tools, instance_tools, or scoped_tools, to ensure consistency with the project's style.
  • Evaluate the dispatch order of extra_tools in relation to hard-coded built-ins and registry fallback, considering the safety default of not allowing extra_tools to shadow critical built-ins.
  • Consider the migration strategy for the existing memory-provider injection, whether to migrate it to extra_tools in the same PR, in a follow-up, or keep it separate.

Example

The proposed implementation in PR #13315 provides a concrete example of how extra_tools can be used to inject domain-specific and instance-bound tools into an AIAgent instance.

Notes

The solution assumes that the proposed implementation in PR #13315 is correct and functional. Further testing and review may be necessary to ensure the stability and security of the new feature.

Recommendation

Apply the workaround by introducing the extra_tools parameter to the AIAgent constructor, as proposed in PR #13315, to allow per-agent tool injection and improve the handling of domain-specific and instance-bound tools. This approach provides a flexible and scalable solution for managing tools in the project.

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 [RFC] Allow per-AIAgent tool injection via extra_tools= (generalize the memory_manager pattern) [2 pull requests, 1 participants]