Skip to content

[BUG] Agent delegates every Chat callback except before_request #872

Description

@danielefrisanco

Basic checks

  • I searched existing issues - this hasn't been reported
  • I can reproduce this consistently
  • This is a RubyLLM bug, not my application code

What's broken?

Agent's def_delegators list covers six of the seven Chat callback
registrations — before_message, after_message, before_tool_call,
after_tool_result, before_fallback, after_fallback — but not
before_request, so agent.before_request { ... } raises NoMethodError while
its six siblings work.

This isn't blocking anything: an agent keeps the chat you hand it, so configuring
the chat before constructing the agent works fine (workaround under "What actually
happened"). It's the asymmetry I'm reporting, since the class documentation
describes the delegating behaviour without an exception:

  • "Agent instances delegate the Chat API (#ask, #complete, #with_tools, and so
    on) to the wrapped chat, which is available via #chat."
  • On GUARDED_OPERATIONS: "The chat operations an agent instance runs through
    its ::rescue_from handlers. Every other Chat method is delegated untouched."

Calling each callback on an agent, on main:

callback respond_to? returns the wrapped chat
before_message true yes
after_message true yes
before_tool_call true yes
after_tool_result true yes
before_fallback true yes
after_fallback true yes
before_request false

It reads like an oversight rather than a decision: the commit that added
before_request touched chat.rb and the protocols but not agent.rb, and the
existing 'delegates callback hooks to the underlying chat' example enumerates
the six callbacks that existed at the time.

The argument against delegating it, in case it was deliberate: before_request
edits the provider's wire format, and agents support with_fallbacks across
providers, so a hook written for one payload shape could misfire after a
fallback. If that's the reason, a line in the Agent docs would settle it and I'm
happy to send that instead.

Either way I have a patch ready — one word in agent.rb plus a test that extends
the existing delegation example rather than adding a spec file (two files, eight
lines). Glad to open the PR, or to close this if the omission is intended.

How to reproduce

No network needed — Chat#render applies the hooks without sending.

require 'ruby_llm'
RubyLLM.configure { |c| c.anthropic_api_key = ENV['ANTHROPIC_API_KEY'] }

class SupportAgent < RubyLLM::Agent
  model 'claude-haiku-4-5'
end

agent = SupportAgent.new

agent.respond_to?(:before_request)
# => false

agent.before_request { |payload| payload[:metadata] = { user_id: 'u-1' } }
# raises NoMethodError

# The six siblings all work on the same object:
agent.before_message { :ok }
# => #<RubyLLM::Chat model: "claude-haiku-4-5" ...>

Expected behavior

agent.before_request { |payload| ... } registers the hook on the wrapped chat
and returns it, like the other six callbacks.

What actually happened

NoMethodError: undefined method `before_request' for
#<SupportAgent:0x00007df7805c7358 @chat=#<RubyLLM::Chat model: "claude-haiku-4-5",
  provider: "anthropic", messages: 0>>
Did you mean?  before_message

Ruby's own suggestion is before_message — one of the callbacks that is
delegated.

Workaround, which reads fine and is what I'm using: configure the chat first and
hand it to the agent.

chat = RubyLLM.chat(model: 'claude-haiku-4-5')
chat.before_request { |payload| payload[:metadata] = { user_id: 'u-1' } }
agent = SupportAgent.new(chat: chat)

agent.ask_later('Hello')
agent.chat.equal?(chat)        # => true, the agent keeps this chat
agent.chat.render[:metadata]   # => {:user_id=>"u-1"}

Environment

- Ruby version: 3.1.4
- RubyLLM version: main @ be80b6f (2.0 development; VERSION still reads 1.16.0)
- Provider (OpenAI, Anthropic, etc.): Anthropic — provider-independent, the payload
  never leaves the process in the repro
- OS: Linux

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions