`Agent.find`で`assume_model_exists`が伝播しないバグを修正
Agent.findでチャット履歴を再ロードした際、クラス設定のassume_model_exists: trueが引き継がれずModelNotFoundErrorが発生するバグが修正されました。新たにapply_assume_model_existsメソッドを導入し、find・sync_instructions!・llm_chat_forの各パスで一貫してフラグを伝播するようになりました。
背景
assume_model_exists: trueは、モデルレジストリに登録されていないカスタムモデルを使用するための設定です。クラスレベルでmodel 'gpt-6.7', provider: :openai, assume_model_exists: trueと宣言しても、Agent.find経由で既存のチャットを再開しようとするとModelNotFoundErrorが発生するという問題が#679で報告されました。
問題の根本は、assume_model_existsがトランジェントなattr_accessorとして定義されていることにあります。ActiveRecordがデータベースからレコードをロードする際、このフィールドはデータベースには保存されないためnil(false相当)にリセットされます。その後apply_configurationがto_llmを呼び出すと、Models.resolveがレジストリ未登録モデルの解決を試みて例外を投げます。
create/create!で使われるwith_rails_chat_recordは**chat_kwargsをレコードのコンストラクタに展開するため同問題は発生しませんでしたが、findパスにはその相当処理がなく、findとsync_instructions!(ID渡し)の両方で同様の抜け漏れがありました。
技術的な変更
apply_assume_model_existsという専用メソッドを追加し、chat_kwargsからassume_model_existsの値をActiveRecordオブジェクトに注入する処理を一箇所に集約しました。
追加されたメソッド (lib/ruby_llm/agent.rb):
def apply_assume_model_exists(chat_object)
return unless chat_kwargs.key?(:assume_model_exists) &&
resolved_chat_model &&
chat_object.is_a?(resolved_chat_model)
chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
end
このメソッドは3つの条件を満たす場合のみ動作します。chat_kwargsにassume_model_existsキーが存在し、resolved_chat_modelが解決されており、かつchat_objectがそのモデルのインスタンスである場合に限り、値を書き込みます。
呼び出し箇所の追加:
# find パス
def find(id, **kwargs)
# ...
record = resolved_chat_model.find(id)
apply_configuration(record, input_values:, persist_instructions: false)
# (空行追加のみ、apply_assume_model_existsはllm_chat_for内で適用)
record
end
# sync_instructions! パス
def sync_instructions!(chat_or_id, **kwargs)
# ...
record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
apply_assume_model_exists(record) # 追加
# ...
end
# llm_chat_for(new(chat:) パスを含む)
def llm_chat_for(chat_object)
apply_assume_model_exists(chat_object) # 追加
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
end
llm_chat_forへの追加により、既存のActiveRecordオブジェクトをchat:引数としてAgent.newに渡すパス(例: MyAgent.new(chat: Chat.find(id)))も同時に修正されています。
テストは3つのシナリオを網羅しています。find直接呼び出し、sync_instructions!へのID渡し、そして再ロードしたレコードをnew(chat:)に渡すケースです。いずれもnot-a-real-modelという実在しないモデル名とassume_model_exists: trueの組み合わせで、例外が発生しないことを検証しています。
設計判断
apply_assume_model_existsとして処理を単一メソッドに集約する方式が採用されました。
find・sync_instructions!・llm_chat_forの各コードパスに個別にインライン代入を書く方法も取れましたが、それは同一ロジックの散在を招きます。専用メソッドにすることで、将来同様のパスが追加された際の適用漏れを防ぎつつ、ガード節による早期リターンで副作用の発生条件を明示しています。
また、chat_kwargs.key?(:assume_model_exists)という存在チェックにより、フラグが未指定の場合はレコードに一切触れない設計になっています。falseを代入するケースも含めて意図的に制御されており、「値がない」と「falseが設定されている」を区別します。
まとめ
この修正は、create系とfind系でチャットオブジェクトの初期化経路が異なることに起因した非対称性を解消したものです。apply_assume_model_existsの導入により、ActiveRecordロード後のトランジェント属性の欠落を補填する処理が統一され、既存チャットを再開する際のModelNotFoundErrorが防がれます。