【ITニュース解説】Your Agent's Command Printed Nothing for 2 Minutes. stdout Wasn't a Terminal
2026年11月07日に「Dev.to」が公開したITニュース「Your Agent's Command Printed Nothing for 2 Minutes. stdout Wasn't a Terminal」について初心者にもわかりやすく解説しています。
ITニュース概要
AIエージェントがコマンド実行時、出力がすぐ見えずタイムアウトすることがある。標準出力がターミナル以外(ファイルやパイプ)だと、データが溜め込まれる「ブロックバッファリング」が働くためだ。エージェントは途中経過を読めず、停止したと誤解する。対策はバッファリングの無効化だ。
ITニュース解説
AIエージェントに何らかの作業を依頼し、エージェントが内部で実行したコマンドが、なぜか途中の進捗を報告せず、そのままタイムアウトしてしまったり、最終的な出力が期待通りに得られなかったりする経験はないだろうか。特に、AIがプログラムの実行結果をリアルタイムで監視し、それに基づいて次の行動を決定するような場面では、この問題はAIの判断を狂わせ、全体のプロセスが停止したり、間違った方向に進んだりする原因となる。この「出力が見えない」という現象は、実はプログラムの「標準出力」と呼ばれる部分の基本的な仕組み、具体的には「バッファリング」という機能が関係している。
多くのプログラムは、処理の途中でユーザーにメッセージや処理結果を表示するために「標準出力(stdout)」という仕組みを利用する。例えば、プログラムが「ステップ1」「ステップ2」と順番に表示しながら進行していくような場合がこれにあたる。しかし、プログラムが出力したい内容を生成したとしても、それが常に即座に画面に表示されるわけではない。多くの場合、出力されるデータは「バッファ」と呼ばれる一時的な記憶領域に一旦貯められる。このバッファは、細切れのデータをまとめて処理することで、システムの効率を高めるための仕組みである。ちょうど、小さな荷物を一つ一つすぐに配送するのではなく、ある程度の量をまとめてトラックに積んでから目的地へ運ぶようなイメージに近い。
この標準出力のバッファリングには、主に「行バッファリング」と「ブロックバッファリング」の二つのモードが存在する。行バッファリングは、出力内容に改行コード(\n)が含まれるたびに、その行までの内容をバッファから実際の出力先へと送る方式だ。これにより、人間が対話的にプログラムを使用する際に、メッセージが一行ずつリアルタイムで表示されるため、非常に分かりやすい。一方、ブロックバッファリングは、バッファがいっぱいになるか、プログラムが明示的にバッファの内容を出力先に送る命令(これを「フラッシュ」と呼ぶ)を出すか、あるいはプログラムが正常に終了するまで、出力をバッファに貯め続ける方式だ。これは、人間が直接見る必要のない大量のデータを効率的に扱う場合や、ファイルへの書き込みなどによく使われる。
どちらのバッファリングモードが適用されるかは、プログラムの標準出力が「端末(ターミナル)」につながっているかどうかで決まることが多い。端末とは、私たちがコマンドを入力し、プログラムの出力を受け取る画面のことだ。C言語の標準ライブラリやPythonの入出力システムは、isatty()という関数を使って、標準出力が端末に接続されているかを判定する。この判定結果に基づき、もし端末に接続されていれば行バッファリング、そうでなければブロックバッファリングを選択するという昔からのルールが、多くのシステムで採用されている。ちなみに、エラーメッセージなど緊急性の高い情報を扱う「標準エラー出力(stderr)」は、通常はバッファリングされずにすぐに表示されるように設計されている。
AIエージェントがプログラムを実行する環境では、この「端末」の有無が重要な問題となる。例えば、Claude CodeのBashツールがコマンドを実行する際、そのコマンドの標準出力は直接ユーザーの端末には接続されていない。代わりに、エージェントが内部で用意した一時的な「作業ファイル」や「パイプ」にリダイレクトされる。この作業ファイルやパイプは端末ではないため、プログラムはisatty()のチェックで「端末につながっていない」と判断し、自動的にブロックバッファリングモードに切り替わってしまうのだ。
その結果、たとえプログラムが「ステップ1」「ステップ2」といった進捗メッセージを1秒ごとに改行付きで出力するように作られていたとしても、それらのメッセージはすべてバッファ内に貯め込まれ、作業ファイルには一切書き出されない。AIエージェントは、コマンドがタイムアウトする2分後などにこの作業ファイルを読み込もうとするが、ファイルは空っぽであるため、何も出力されなかったと判断してしまう。プログラム自体は内部で正しく動作しているのに、AIエージェントにはその進捗や結果が全く見えないという、困った状況が発生するわけだ。さらに、コマンドがタイムアウトなどによって強制終了された場合、バッファに貯まっていた未出力の内容は、フラッシュされることなくそのまま失われてしまう可能性がある。通常の終了処理ではバッファがフラッシュされるが、強制終了ではそれが保証されないため、AIエージェントはプログラムが何も処理しなかったと誤解し、その後の行動を誤るリスクが高まる。
この現象は、簡単な実験で確認できる。例えば、C言語で1秒ごとに「step X」と出力し、sleep(1)で待機するプログラムを作成し、./tickとして直接実行すれば、出力は1秒ごとに表示される。しかし、このプログラムの出力をcatコマンドにパイプで渡して./tick | catと実行すると、5秒間何も表示されず、プログラムが終了する瞬間にすべての出力が一気に表示されるはずだ。これは、catコマンドへのパイプが端末ではないためブロックバッファリングが適用され、プログラム終了時にバッファがフラッシュされて初めて出力されたからだ。Pythonの場合も同様で、print文を含むスクリプトをpython3 script.pyで実行すると逐次表示されるが、python3 script.py | catとするとまとめて表示される。さらに、timeoutコマンドを使ってtimeout 3 python3 script.py | catのように3秒で強制終了させると、バッファされた出力は一切表示されない。
この問題を解決し、AIエージェントにプログラムの進捗をリアルタイムで伝えるためには、いくつかの対策を講じる必要がある。
Pythonプログラムの場合、最も簡単な方法は、Pythonインタープリタに-uオプションを付けて実行することだ。python3 -u script.pyとすると、標準出力と標準エラー出力のバッファリングが無効になり、出力がすぐに送られるようになる。また、環境変数PYTHONUNBUFFEREDを1に設定することでも同じ効果が得られる。プログラム内のprint関数をprint(f"step {i}", flush=True)のようにflush=Trueオプションを付けて呼び出すことで、明示的にバッファをフラッシュさせることも可能だ。AIエージェント環境でこれらの設定を常に適用したい場合は、Claude Codeのプロジェクト設定ファイル(.claude/settings.jsonなど)に環境変数を記述する方法が有効だ。
C言語で書かれた自身のプログラムの場合は、main関数の冒頭でsetvbuf(stdout, NULL, _IOLBF, 0);という関数を呼び出すことで、標準出力を強制的に行バッファリングモードに設定できる。あるいは、各出力の後にfflush(stdout);を呼び出して、明示的にバッファをフラッシュすることもできる。他人が作成したC言語ベースのバイナリプログラムで、Linux環境であればstdbufコマンドが役に立つ。例えば、stdbuf -oL ./tick | catと実行することで、出力がパイプにつながっていても行バッファリングのように振る舞うようになる。ただし、stdbufはプログラムの内部的な挙動によっては効果がない場合もあるため注意が必要だ。
最終手段として、scriptコマンドを使って擬似端末(PTY)をプログラムに提供する方法もある。これにより、たとえパイプ経由で実行されたとしても、プログラムは自分が端末に接続されていると誤解し、行バッファリングに切り替わる。script -q -c "./tick" /dev/nullのように使用するが、これは複雑さを増すため、他の解決策が難しい場合に検討すべきだろう。
AIエージェントが正確な情報を得て賢く動作するためには、実行するコマンドがリアルタイムで出力を生成し、それをエージェントが読み取れる状態にすることが不可欠だ。これらの標準出力のバッファリングに関する知識と適切な対策は、AIエージェントシステムを開発・運用する上で非常に重要な基礎となるだろう。この問題の理解は、単にAIエージェントだけでなく、一般的なシステム開発における入出力処理の効率と信頼性を高める上でも役立つ。