Skip to content

Fix acts_as_message inflection for parent_tool_call causes nil association#691

Open
jayelkaake wants to merge 1 commit intocrmne:mainfrom
jayelkaake:main
Open

Fix acts_as_message inflection for parent_tool_call causes nil association#691
jayelkaake wants to merge 1 commit intocrmne:mainfrom
jayelkaake:main

Conversation

@jayelkaake
Copy link
Contributor

@jayelkaake jayelkaake commented Mar 19, 2026

What this does

  • add parent_tool_call_foreign_key to acts_as_message and use that value if specified, otherwise fall back on original assumtpion.

Bug Description

When you have a custom column name for the parent_tool_call_id foreign key, the current code does:

belongs_to :parent_tool_call,
           class_name: self.tool_call_class,
           foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
           optional: true

but ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize) doesn't assume correctly if tool_calls == :tool_call but the tool_call_class: 'SomethingElse'.

Recreation steps

Setup:

before(:all) do # rubocop:disable RSpec/BeforeAfterAll
  ActiveRecord::Migration.suppress_messages do
    # ...

    ActiveRecord::Migration.create_table :ai_tool_calls, force: true do |t|
      t.references :ai_message
      t.string :ai_tool_call_id
      # ...
    end

    ActiveRecord::Migration.create_table :ai_messages, force: true do |t|
      t.references :ai_chat
      # ...
      t.integer :ai_tool_call_id # <<< Note this
    end

    # ...

  end
end

# ...

class AiMessage
  acts_as_message(
    chat_class: 'AiChat',
    chat_foreign_key: :ai_chat_id,
    tool_call_class: 'AiToolCall', 
    tool_calls_foreign_key: :ai_message_id,
    model_class: 'AiModel', 
    model_foreign_key: 'AiModel')
  # ...
end    

# ...

Test:

AiMessage.where("ai_tool_call_id IS NOT NULL").first.parent_tool_call

  • Expected result: instance of AiToolCall
  • Current result: nil

Solution

Considered updating the inflection method, but decided it's better to be explicit in this non-standard case. Configuration over convention for once since it's more clear.

Details

Type of change

  • Bug fix
  • New feature
    • (since it adds a new config option)
  • Breaking change
  • Documentation
  • Performance improvement

Scope check

  • I read the Contributing Guide
  • This aligns with RubyLLM's focus on LLM communication
  • This isn't application-specific logic that belongs in user code
  • This benefits most users, not just my specific use case

Quality check

  • I ran overcommit --install and all hooks pass
  • I tested my changes thoroughly
    • For provider changes: Re-recorded VCR cassettes with bundle exec rake vcr:record[provider_name]
    • All tests pass: bundle exec rspec
  • I updated documentation if needed
  • I didn't modify auto-generated files manually (models.json, aliases.json)

AI-generated code

  • I used AI tools to help write this code
    • I generated the spec with claude's help. Checked it and tested edge cases. We should split up that file in the future.
  • I have reviewed and understand all generated code (required if above is checked)

API changes

  • Breaking change
  • New public methods/classes
  • Changed method signatures
    • added to method signature... does that count?
  • No API changes

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant