From 585ffbec5a52efa0d72658184c4fff1c4792dbfa Mon Sep 17 00:00:00 2001 From: Gujiassh Date: Sun, 22 Mar 2026 16:25:00 +0900 Subject: [PATCH] Fix parent_tool_call foreign key inference Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- lib/ruby_llm/active_record/acts_as.rb | 8 ++++- spec/ruby_llm/active_record/acts_as_spec.rb | 39 ++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/ruby_llm/active_record/acts_as.rb b/lib/ruby_llm/active_record/acts_as.rb index 2dfdbac37..43a9cb903 100644 --- a/lib/ruby_llm/active_record/acts_as.rb +++ b/lib/ruby_llm/active_record/acts_as.rb @@ -106,6 +106,12 @@ def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_c self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s self.model_class = (model_class || model.to_s.classify).to_s + parent_tool_call_foreign_key = if tool_calls_foreign_key&.end_with?('_message_id') + tool_calls_foreign_key.sub(/_message_id\z/, '_tool_call_id') + else + ActiveSupport::Inflector.foreign_key(self.tool_call_class) + end + belongs_to chat, class_name: self.chat_class, foreign_key: chat_foreign_key, @@ -118,7 +124,7 @@ def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_c belongs_to :parent_tool_call, class_name: self.tool_call_class, - foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize), + foreign_key: parent_tool_call_foreign_key, optional: true has_many :tool_results, diff --git a/spec/ruby_llm/active_record/acts_as_spec.rb b/spec/ruby_llm/active_record/acts_as_spec.rb index 1bd648ec9..e344341a9 100644 --- a/spec/ruby_llm/active_record/acts_as_spec.rb +++ b/spec/ruby_llm/active_record/acts_as_spec.rb @@ -512,15 +512,17 @@ class BotToolCall < ActiveRecord::Base # rubocop:disable Lint/ConstantDefinition after(:all) do # rubocop:disable RSpec/BeforeAfterAll ActiveRecord::Migration.suppress_messages do - if ActiveRecord::Base.connection.table_exists?(:support_tool_calls) - ActiveRecord::Migration.drop_table :support_tool_calls - end + ActiveRecord::Base.connection.execute('PRAGMA foreign_keys = OFF') if ActiveRecord::Base.connection.table_exists?(:support_messages) ActiveRecord::Migration.drop_table :support_messages end + if ActiveRecord::Base.connection.table_exists?(:support_tool_calls) + ActiveRecord::Migration.drop_table :support_tool_calls + end if ActiveRecord::Base.connection.table_exists?(:support_conversations) ActiveRecord::Migration.drop_table :support_conversations end + ActiveRecord::Base.connection.execute('PRAGMA foreign_keys = ON') end end @@ -534,7 +536,12 @@ class Conversation < ActiveRecord::Base # rubocop:disable RSpec/LeakyConstantDec end class Message < ActiveRecord::Base # rubocop:disable RSpec/LeakyConstantDeclaration - acts_as_message chat: :conversation, chat_class: 'Support::Conversation', tool_call_class: 'Support::ToolCall' + acts_as_message( + chat: :conversation, + chat_class: 'Support::Conversation', + tool_calls: :stored_calls, + tool_call_class: 'Support::ToolCall' + ) end class ToolCall < ActiveRecord::Base # rubocop:disable RSpec/LeakyConstantDeclaration @@ -548,6 +555,30 @@ class ToolCall < ActiveRecord::Base # rubocop:disable RSpec/LeakyConstantDeclara expect { conversation.messages.create!(role: 'user', content: 'Test') }.not_to raise_error expect(conversation.messages.count).to eq(1) end + + it 'uses the tool_call_class-derived foreign key for parent_tool_call' do + association = Support::Message.reflect_on_association(:parent_tool_call) + + expect(association.foreign_key).to eq('tool_call_id') + end + + it 'persists parent_tool_call with a custom tool_calls association name' do + conversation = Support::Conversation.create!(model: model) + message = conversation.messages.create!(role: 'assistant', content: 'Tool call message') + tool_call = message.stored_calls.create!( + tool_call_id: 'call_123', + name: 'calculator', + arguments: { expression: '2 + 2' } + ) + + expect do + conversation.messages.create!( + role: 'tool', + content: '4', + parent_tool_call: tool_call + ) + end.not_to raise_error + end end describe 'to_llm conversion' do