0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

convertToModelMessagesの非同期化、型なしだと壊れたまま動いていた

0
Last updated at Posted at 2026-07-10

はじめに

Vercel AI SDK(ai パッケージ)は npm 週間ダウンロード数 1,514万件(2026-06-29〜07-05・npm公式レジストリAPI実測)のTypeScript向けAIツールキットです1。メジャーバージョン6でエージェント抽象化 ToolLoopAgent が追加された一方、地味だが実害の大きい破壊的変更が convertToModelMessages() に入っていました。

同期関数だったものが非同期関数になり、しかも呼び出し側が型チェックをしていないと、エラーを一切出さずに壊れたまま動き続けます。 実際に手元でv6をインストールして検証したので、その結果を共有します。

この記事で分かること

  • convertToModelMessages() がv6で非同期化した事実(型定義・実行結果で確認済み)
  • 型なし(JS・any)のコードがどう壊れるか、実際のログで確認
  • TypeScriptなら型エラーで守られる、その具体的なエラーメッセージ
  • ToolLoopAgent のデフォルト stopWhen も地味に変わっている点

前提環境

  • Node.js v22.22.2
  • ai パッケージ v6.0.221(2026年7月時点。npmのlatestは既に7系だが、6系はまだ多くのプロダクションコードで稼働中)

TL;DR

  • convertToModelMessages() はv5では同期的に配列を返していたが、v6では Promise<ModelMessage[]> を返す非同期関数になった
  • await を付け忘れても 実行時エラーにならない。Promiseオブジェクトがそのまま次の処理に渡り、LLM APIへのリクエストが壊れた形で送られる
  • TypeScriptの型チェックを通していれば error TS2740 で検出できるが、any 型や素のJavaScriptでは気づけない
  • ToolLoopAgent のデフォルト stopWhenstepCountIs(20) に変更されており、旧 Experimental_Agent の1ステップ止まりから挙動が変わっている

背景: なぜ非同期化されたか

AI SDK 6のマイグレーションガイドによると、convertToModelMessages() の非同期化は Tool.toModelOutput() の非同期対応が理由です2。ツールの出力をモデル向けメッセージに変換する処理が非同期になったことで、それを呼び出す convertToModelMessages() 自体も連鎖的に非同期化されました。

設計としては筋が通っていますが、問題は 呼び出し側のコード です。v5時代に書かれた次のようなコードは、v6にアップグレードしても文法上は何のエラーも出さずに動き続けます。

// v5時代の書き方(同期呼び出しのつもり)
const messages = convertToModelMessages(uiMessages);
console.log(messages.length); // ← ここで初めて壊れ方に気づく

実機検証1: 同期呼び出しの結果を見る

手元に ai@6.0.221 をインストールし、実際に同期呼び出しを試しました。

npm install ai@6 zod@3
import { convertToModelMessages } from 'ai';

const uiMessages = [
  { role: 'user', parts: [{ type: 'text', text: 'こんにちは' }] },
];

const result = convertToModelMessages(uiMessages);
console.log('typeof result:', typeof result);
console.log('is Promise:', result instanceof Promise);
console.log('result:', result);

実行結果は次の通りです。

typeof result: object
is Promise: true
result: Promise { [ { role: 'user', content: [Array] } ] }

例外は一切発生しません。 result はオブジェクト(Promise)として扱えてしまうため、result.map(...) のような配列メソッド呼び出しをして初めて TypeError: result.map is not a function に気づく、という順序になります。ツール呼び出しを含む複雑なエージェントコードでは、この配列アクセスがネストした場所にあることも多く、発見が遅れがちです。

実機検証2: TypeScriptなら型エラーで守られる

同じコードをTypeScriptの型付きで書くとどうなるか確認しました。

import { convertToModelMessages, type ModelMessage } from 'ai';

const uiMessages = [
  { role: 'user' as const, parts: [{ type: 'text' as const, text: 'こんにちは' }] },
];

const messages: ModelMessage[] = convertToModelMessages(uiMessages);
console.log(messages);

npx tsc --noEmit を実行すると、次のエラーで即座に検出できます。

error TS2740: Type 'Promise<ModelMessage[]>' is missing the following properties
from type 'ModelMessage[]': length, pop, push, concat, and 29 more.

つまり 型チェックさえ通していれば絶対に見逃さない変更 です。逆に言えば、any 型で受けていたり、// @ts-ignore で握りつぶしていたり、そもそもJavaScriptで書いていたりすると、この保護が働きません。CIに tsc --noEmit を組み込んでいないプロジェクトは、今回のアップグレードで最も刺さりやすい部類です。

実機検証3: ToolLoopAgentのデフォルトstopWhenも変わっている

ついでにv6の新しいエージェント抽象化 ToolLoopAgent のソースも確認しました。node_modules/ai/dist/index.mjs を直接grepすると、デフォルトの停止条件が次のように実装されています。

stopWhen: (_a22 = this.settings.stopWhen) != null ? _a22 : stepCountIs(20),

Experimental_Agent(v5の実験的エージェントAPI)はデフォルトが1ステップ(isStepCount(1))でしたが、ToolLoopAgent では デフォルト20ステップ に変わっています2stopWhen を明示的に指定していないエージェントは、アップグレード後にツール呼び出しを以前よりずっと長く自律的に繰り返すようになる可能性があります。コスト・レイテンシに直結するため、移行時は必ず stopWhen: stepCountIs(N) を明示することをおすすめします。

なぜ「壊れたまま動く」のか

今回の一連の変更に共通しているのは、JavaScriptの動的型付けが「意味的に間違ったオブジェクト」を黙って受け入れてしまう という性質です。Promiseは .then() を持つオブジェクトとして扱われるため、「配列を渡したつもり」のコードは実行はできてしまいます。エラーになるのは、Promiseに配列メソッド(.map().length)を呼んだ瞬間か、あるいはそのままLLM APIにシリアライズされて送信され、プロバイダー側から「不正なメッセージフォーマット」としてエラーが返ってきた瞬間です。後者の場合、原因が自分のコードにあると気づくまでに時間がかかります。

対策

  1. 公式のcodemodを使う: npx @ai-sdk/codemod v6 で機械的な移行の大部分をカバーできます2
  2. convertToModelMessages の呼び出し箇所を全て grep: await の有無を目視で確認する。
  3. TypeScriptの strict モードを有効化し、CIで tsc --noEmit を必ず回す: 今回のような「実行時に気づきにくい」変更を型エラーで先回りできる唯一の防御線です。
  4. ToolLoopAgent / Agent を使う箇所は stopWhen を明示指定する: デフォルト値の変更に依存しない。

著者視点の発見ポイント

今回手を動かして一番驚いたのは、Promiseを配列として扱おうとしても 即座にはエラーにならない という点でした。console.log(result) の出力を見て初めて Promise { [...] } という表示に気づく形で、ぱっと見では気づきにくい罠です。「型のないコードほど、非同期化のような変更に弱い」という当たり前の事実を、実際に手を動かして再確認できました。TypeScriptのメジャーアップグレードでは、変更ログを読むだけでなく tsc --noEmit を一度実行してから本番投入することを徹底したいと思います。

まとめ

  • Vercel AI SDK 6で convertToModelMessages() が非同期化。呼び出し側で await を忘れても実行時エラーにならず、Promiseオブジェクトがそのまま流れてしまう
  • TypeScriptの型チェック(tsc --noEmit)を通していれば error TS2740 で確実に検出できる
  • ToolLoopAgent のデフォルト stopWhenstepCountIs(20) に変更されており、意図しない自律ループの長期化に注意
  • メジャーバージョンアップ時は公式codemodの実行+型チェックCIの整備がセットで必須

参考リンク

  1. npm registry APIai パッケージ、2026-06-29〜2026-07-05の週間ダウンロード数を実測)

  2. Migration Guide: AI SDK 5.x to 6.0(2026年7月確認) 2 3

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?