Skip to content

Feature: Enhanced Extension System with Tool Interception & Lifecycle Events (inspired by Pi) #359

@teknium1

Description

@teknium1

Overview

Inspired by the Pi coding agent (GitHub), this proposes significantly enhancing Hermes Agent's hook/extension system to support tool interception, context modification, event cancellation, and richer lifecycle events. Pi's extension system is its deepest architectural strength -- it provides 30+ lifecycle events with the ability to block tool calls, modify tool results, transform context before it reaches the LLM, and even replace UI components.

Hermes already has a hook system (gateway/hooks.py) with 7 event types, but it's fire-and-forget with no return values or cancellation. This proposal would evolve it into a full extension API that enables powerful plugins: approval gates, audit logging, custom sandboxing, context injection, tool result transformation, and more.

This builds on the existing hook infrastructure rather than replacing it, and complements the existing tool registry and skills systems.


Research Findings

How Pi's Extension System Works

Pi extensions are TypeScript modules that receive an ExtensionAPI object with deep hooks into the agent lifecycle:

Event System (30+ events):

Category Events Can Cancel/Modify?
Session lifecycle session_start, session_before_switch, session_switch, session_before_compact, session_compact, session_shutdown Yes (before_ events)
Agent lifecycle before_agent_start, agent_start, agent_end, turn_start, turn_end Yes (before_ events)
Message streaming message_start, message_update, message_end No (observe only)
Tool interception tool_call (can BLOCK), tool_result (can MODIFY) Yes -- this is key
Input processing input (can transform or handle user input) Yes
Context context (can modify messages before LLM) Yes -- modify context
Model events model_select Yes
Bash execution user_bash (can replace execution) Yes

Tool Interception Pattern:

// Extension can intercept any tool call
pi.on('tool_call', async (event) => {
  // Block dangerous commands
  if (event.tool === 'bash' && event.args.command.includes('rm -rf')) {
    event.block('Dangerous command blocked by safety extension');
    return;
  }
  // Or modify arguments before execution
  event.args.command = wrapInSandbox(event.args.command);
});

// Modify tool results before they reach the LLM
pi.on('tool_result', async (event) => {
  event.result = sanitize(event.result);
});

Context Modification Pattern:

// Inject or modify messages right before they go to the LLM
pi.on('context', async (event) => {
  // Add dynamic context based on current state
  event.messages.push({
    role: 'user',
    content: `Current git branch: ${await getGitBranch()}`
  });
});

Registration APIs:

  • pi.registerTool() -- add LLM-callable tools dynamically
  • pi.registerCommand() -- add slash commands
  • pi.registerShortcut() -- add keyboard shortcuts
  • pi.registerProvider() -- add model providers with OAuth support
  • pi.setActiveTools() / pi.getActiveTools() -- dynamic tool management
  • pi.events -- shared EventBus for inter-extension communication

Extension Discovery:

  • Project-local: .pi/extensions/
  • Global: ~/.pi/agent/extensions/
  • Package manager: pi install npm:@foo/pi-tools

Key Design Decisions

  1. Before/after event pattern -- before_X events can cancel the operation; X events fire after completion (observe only). Clean separation of concerns.
  2. Tool interception as first-class -- Any tool call can be blocked or modified by extensions. This enables approval gates, sandboxing, logging without modifying tool code.
  3. Context as an event -- The message array going to the LLM is exposed as a modifiable event, enabling dynamic context injection from any extension.
  4. Extension isolation -- Extensions share an EventBus but can't directly access each other. Communication is event-driven.

Current State in Hermes Agent

Existing Hook System (gateway/hooks.py)

Hermes has a lightweight hook system:

  • Discovery: File-based, ~/.hermes/hooks/<hook-name>/ with HOOK.yaml + handler.py
  • 7 event types: gateway:startup, session:start, session:reset, agent:start, agent:step, agent:end, command:*
  • Dispatch: emit(event_type, context) fires all matching handlers
  • Sync + async handler support
  • Wildcard matching (command:* matches any command:X)
  • Fire-and-forget -- errors caught and logged, never block pipeline
  • No return values, no cancellation, no modification

Other Extension Points

  • Tool Registry (tools/registry.py): Self-registering tools at import time. ToolEntry with schema, handler, check_fn. Toolset-based grouping.
  • Skills System: On-demand knowledge docs, agentskills.io compatible, Skills Hub for install
  • Context Files: SOUL.md, AGENTS.md, .cursorrules loaded into system prompt
  • Callbacks: tool_progress_callback, clarify_callback, step_callback in AIAgent
  • Memory System: Persistent MEMORY.md + USER.md

The Gap

The hook system is observe-only. There's no way for extensions to:

  • Block or approve tool calls before execution
  • Modify tool results before they reach the LLM
  • Inject dynamic context into the message array
  • Register new tools at runtime
  • Add custom slash commands from extensions
  • Communicate between extensions via events

Implementation Plan

Skill vs. Tool Classification

This is a core codebase change -- it enhances the fundamental extension architecture of the agent. It touches gateway/hooks.py, run_agent.py (the agent loop), tools/registry.py (tool dispatch), and gateway/run.py (command handling). Cannot be expressed as a skill or tool.

What We'd Need

  • Enhanced event system with return values, cancellation, and priority ordering
  • Tool call/result interception points in the agent loop
  • Context modification hook before LLM API calls
  • Extension manifest format (beyond HOOK.yaml)
  • Dynamic tool and command registration APIs
  • Inter-extension event bus

Phased Rollout

Phase 1: Enhanced Event System

  • Upgrade HookRegistry to support return values from handlers
  • Add before_ event pattern: before_tool_call, before_agent_start, before_compress
  • before_ handlers can return {"cancel": True, "reason": "..."} to block the operation
  • Add priority ordering to handlers (lower number = earlier execution)
  • Add tool_call and tool_result events with modification capability
  • Maintain backward compatibility -- existing hooks work unchanged

Phase 2: Context & Tool Hooks

  • Add context event fired in run_agent.py before building API messages
    • Handlers receive the message array and can modify it (add/remove/edit messages)
    • Enables dynamic context injection (git state, environment info, custom data)
  • Add input event for user message preprocessing
  • Add runtime tool registration: register_tool(name, schema, handler) via extension API
  • Add runtime command registration: register_command(name, handler) via extension API

Phase 3: Extension Ecosystem

  • Extension manifest format (EXTENSION.yaml with metadata, dependencies, events)
  • Extension package manager (hermes extensions install <source>)
  • Inter-extension event bus for extension-to-extension communication
  • Extension UI hooks for CLI mode (status bar, custom widgets)
  • Built-in extensions: audit logger, safety gate, git checkpoint

Pros & Cons

Pros

  • Powerful plugin ecosystem -- Enables approval gates, sandboxing, audit logging, custom providers without modifying core code
  • Safety without rigidity -- Tool interception allows per-project or per-user security policies via extensions rather than baked-in permission popups
  • Dynamic context -- Extensions can inject real-time project state (git branch, test results, linter output) into LLM context
  • Backward compatible -- Existing hooks continue working; new capabilities are opt-in
  • Community extensibility -- Users can share extensions for specific workflows, tools, or integrations

Cons / Risks

  • Complexity cost -- More extension points = more surface area for bugs, harder to reason about behavior
  • Performance -- Synchronous hook chains before tool calls add latency; need careful async design
  • Security -- Extensions with tool interception have significant power; need trust model
  • Ordering issues -- Multiple extensions modifying the same event can create conflicts; priority system helps but doesn't eliminate
  • Maintenance burden -- Extension API becomes a contract that's hard to change once published

Open Questions

  • Should extension-registered tools be visible alongside built-in tools, or namespaced (e.g., ext:tool_name)?
  • Should there be an extension permissions model (which events an extension can subscribe to)?
  • How should extension conflicts be handled (two extensions modifying the same tool result)?
  • Should extensions be able to modify the system prompt, or only the message array?
  • What's the right trust boundary -- should extensions run in a sandbox or have full access?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions