Skip to content

Commit 6e80976

Browse files
Fix callback persistence chaining using Fanout pattern
When using acts_as_chat, persistence callbacks were being overwritten when users added their own callbacks via on_new_message, on_end_message, etc. This caused silent data loss. The fix adds a simple CallbackFanout class that appends callbacks instead of replacing them, so both persistence and user callbacks run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent fa10f0c commit 6e80976

File tree

5 files changed

+155
-38
lines changed

5 files changed

+155
-38
lines changed

lib/ruby_llm/active_record/acts_as_legacy.rb

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,26 +152,12 @@ def with_schema(...)
152152
end
153153

154154
def on_new_message(&block)
155-
to_llm
156-
157-
existing_callback = @chat.instance_variable_get(:@on)[:new_message]
158-
159-
@chat.on_new_message do
160-
existing_callback&.call
161-
block&.call
162-
end
155+
to_llm.on_new_message(&block)
163156
self
164157
end
165158

166159
def on_end_message(&block)
167-
to_llm
168-
169-
existing_callback = @chat.instance_variable_get(:@on)[:end_message]
170-
171-
@chat.on_end_message do |msg|
172-
existing_callback&.call(msg)
173-
block&.call(msg)
174-
end
160+
to_llm.on_end_message(&block)
175161
self
176162
end
177163

lib/ruby_llm/active_record/chat_methods.rb

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -140,26 +140,12 @@ def with_schema(...)
140140
end
141141

142142
def on_new_message(&block)
143-
to_llm
144-
145-
existing_callback = @chat.instance_variable_get(:@on)[:new_message]
146-
147-
@chat.on_new_message do
148-
existing_callback&.call
149-
block&.call
150-
end
143+
to_llm.on_new_message(&block)
151144
self
152145
end
153146

154147
def on_end_message(&block)
155-
to_llm
156-
157-
existing_callback = @chat.instance_variable_get(:@on)[:end_message]
158-
159-
@chat.on_end_message do |msg|
160-
existing_callback&.call(msg)
161-
block&.call(msg)
162-
end
148+
to_llm.on_end_message(&block)
163149
self
164150
end
165151

lib/ruby_llm/chat.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ class Chat
77

88
attr_reader :model, :messages, :tools, :params, :headers, :schema
99

10+
# Stores multiple callbacks per key and invokes all of them
11+
class CallbackFanout
12+
def initialize
13+
@callbacks = {}
14+
end
15+
16+
def [](key)
17+
callbacks = @callbacks[key]
18+
return if callbacks.nil? || callbacks.empty?
19+
20+
->(*args) { callbacks.each { |cb| cb.call(*args) } }
21+
end
22+
23+
def []=(key, callable)
24+
return unless callable
25+
26+
(@callbacks[key] ||= []) << callable
27+
end
28+
end
29+
1030
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
1131
if assume_model_exists && !provider
1232
raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
@@ -22,12 +42,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n
2242
@params = {}
2343
@headers = {}
2444
@schema = nil
25-
@on = {
26-
new_message: nil,
27-
end_message: nil,
28-
tool_call: nil,
29-
tool_result: nil
30-
}
45+
@on = CallbackFanout.new
3146
end
3247

3348
def ask(message = nil, with: nil, &)

spec/fixtures/vcr_cassettes/activerecord_actsas_event_callbacks_allows_chaining_callbacks_on_to_llm_without_losing_persistence.yml

Lines changed: 116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/ruby_llm/active_record/acts_as_spec.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,20 @@ def uploaded_file(path, type)
529529
expect(chat.messages.count).to eq(2) # Persistence still works
530530
end
531531

532+
it 'allows chaining callbacks on to_llm without losing persistence' do
533+
chat = Chat.create!(model: model)
534+
llm_chat = chat.to_llm
535+
536+
user_callback_called = false
537+
# Directly attach callback to the underlying Chat object
538+
llm_chat.on_new_message { user_callback_called = true }
539+
540+
chat.ask('Hello')
541+
542+
expect(user_callback_called).to be true
543+
expect(chat.messages.count).to eq(2) # Persistence still works
544+
end
545+
532546
it 'calls on_tool_call and on_tool_result callbacks' do
533547
tool_call_received = nil
534548
tool_result_received = nil

0 commit comments

Comments
 (0)