Microsoft Agent Framework for Pythonで、store=Falseなどのステートレス実行を使っていると、推論モデルが生成したtool callを次のターンで再生できず、HTTP 400になることがありました。
結論からいうと、この問題はMicrosoft Agent Framework for Pythonリリース1.12.1で修正されています。agent-framework-coreだけでなく、agent-framework-foundry、agent-framework-foundry-hosting、agent-framework-openaiにまたがる修正です。該当する環境では、関連パッケージを整合した状態へ更新し、更新前に保存したセッションやチェックポイントも含めて再検証する必要があります。(GitHub)
Agent Framework Python 1.12.1でステートレスtool call再生を修正
Microsoft Agent Framework for Pythonの1.12.1では、推論項目と対になったツール呼び出し、いわゆるreasoning-paired tool callをステートレス環境で再生する処理が修正されました。
影響対象としてリリースノートに記載されているのは、次の4パッケージです。
| パッケージ | 主な役割 | 今回の修正との関係 |
|---|---|---|
agent-framework-core | メッセージ、コンテンツ、ワークフローなどの共通基盤 | 推論IDや暗号化された推論データを失わずに保持する |
agent-framework-openai | OpenAI Responses APIとの変換・通信 | ステートレス実行時に推論項目とtool callを正しく再構築する |
agent-framework-foundry | Microsoft Foundryのエージェント連携 | 複数エージェント間の履歴再生を正常化する |
agent-framework-foundry-hosting | Foundry Agent Server上でのホスティング | ホスト経由でも推論メタデータを保持して入出力する |
特に問題になっていたのは、クライアント側で実行する通常のfunction_callと、Foundry側で処理されるmcp_callです。1.12.1では両方が修正対象になっています。(GitHub)
問題が発生するステートレス実行とは
Agent Frameworkの会話継続方法は、大きく分けるとステートフルとステートレスの2種類があります。
| 実行方法 | 会話履歴の保持場所 | 次のリクエストで必要な処理 |
|---|---|---|
| ステートフル | OpenAIやFoundryなどのサービス側 | previous_response_idや会話IDで前回の状態を参照する |
| ステートレス | アプリケーション側 | 必要な会話履歴やtool call情報をリクエスト内で再送する |
サービス側の会話保存を利用する場合、前回の推論項目やtool callはサービス側に残っています。そのため、クライアントが同じ項目をもう一度送ると、重複IDとして拒否される可能性があります。
一方、store=Falseでサービス側に履歴を保存しない場合は、クライアント側が前回までの状態を復元しなければなりません。推論モデルを使う場合、単なるテキスト履歴だけでなく、推論項目、tool call、tool resultの対応関係も維持する必要があります。1.12.1の実装では、サービス側の継続情報がないときにreasoning.encrypted_contentを取得対象へ自動追加する処理が入っています。
reasoning-paired tool callとは
推論モデルの応答では、ツール呼び出しが独立した項目として生成されるとは限りません。次のように、推論項目とtool callがひとまとまりとして扱われる場合があります。
reasoning
└─ function_call または mcp_call
└─ function_call_output または MCPの実行結果
ここでいうreasoningは、アプリケーションが読むための完全な思考過程を意味するものではありません。Responses APIが会話を継続するために使用する、IDや暗号化された不透明な推論データを含む項目です。
この組み合わせでは、tool callだけを次のリクエストへ送ることはできません。tool callが特定のreasoning項目に関連付けられている場合、関連するreasoning項目も一緒に渡す必要があります。
修正前にHTTP 400が発生した理由
修正前のステートレス再生では、Agent Frameworkが履歴からtext_reasoningを取り除く一方で、関連するfunction_callやmcp_callを残すことがありました。
処理の流れは次のようになります。
前回の正常な応答
reasoning + tool call + tool result
次のリクエストを作成
tool call + tool result
※reasoningだけが欠落
Foundry側で検証
「必要なreasoning項目が存在しない」と判断
HTTP 400
実際に報告された問題では、mcp_callに必要なreasoning項目が含まれていないため、Azure側がリクエストをHTTP 400で拒否していました。
この現象は毎回発生するわけではありません。推論モデルがreasoningとtool callを対にした形式で出力した場合にだけ表面化するため、開発環境では再現しにくくても、会話回数やtool call回数が増える本番環境では発生確率が高くなります。(GitHub)
Python 1.12.1で変更された処理
暗号化された推論データを取得する
ステートレス実行では、Agent Frameworkがincludeへ次の値を追加します。
"reasoning.encrypted_content"
これにより、サービス側に会話を保存しなくても、次のターンでreasoning項目を再構築するための不透明なデータを取得できます。
アプリケーション側で同じ値をすでに設定している場合は、重複して追加されません。
推論IDとprotected_dataを保持する
取得した暗号化データは、Agent FrameworkのContentにあるprotected_dataへ保存されます。
また、ストリーミング応答の結合やシリアライズを行っても、reasoning項目のIDとprotected_dataが失われないように処理されています。これにより、次のターンで元のreasoning項目を識別できます。
reasoning項目を再構築して一度だけ送信する
ステートレス実行時には、履歴内のtext_reasoningがプロバイダーのreasoning IDごとにまとめられます。
その後、次の情報を含むreasoning項目として再構築されます。
type
id
summary
encrypted_content
status
同じreasoning IDが複数のContentへ分割されていても、送信時には一つのreasoning項目へまとめられます。tool callとtool resultも対応関係を保ったまま再生されます。
再構築できない履歴を送信前に拒否する
1.12.1では、必要な暗号化推論データが欠落している場合、壊れたリクエストをサービスへ送信する前にChatClientInvalidRequestExceptionを発生させます。
エラーメッセージには、ステートレス再生で推論グループを再構築できないことや、次の対処方法が示されます。
- サービス側の会話継続を利用する
- reasoningとtool callのグループをまとめて除外する
- 欠落した履歴を使用せず、新しいセッションを開始する
HTTP 400になるまで処理を進めるのではなく、アプリケーション側で原因を切り分けやすくなった点も重要です。
実行中のtool loopは維持される
修正では、過去の履歴として再生するtool callと、現在実行中のtool loopが区別されています。
そのため、正常に進行しているクライアント側の関数呼び出しまで削除されるわけではありません。非推論モデルのtool call履歴についても、従来の挙動を維持する方針になっています。(GitHub)
パッケージごとの具体的な影響
agent-framework-core
agent-framework-coreでは、推論コンテンツを結合するときにIDとprotected_dataを保持できるようになっています。
ストリーミング結果やチェックポイントからメッセージを復元した場合でも、暗号化された推論データを次の処理へ引き継ぐための基盤部分です。
agent-framework-openai
今回の修正で中心的な役割を持つパッケージです。
主に次の処理を担当します。
- ステートレスかステートフルかを判定する
- ステートレス時に
reasoning.encrypted_contentを要求する - 保存された推論コンテンツをreasoning項目へ再構築する
function_callとfunction_call_outputを正しく再送するmcp_callとその結果を正しくまとめる- 再構築不可能な履歴を送信前に検出する
OpenAI Responses APIを直接利用する構成だけでなく、Foundry統合から内部的にOpenAIクライアントを利用する構成にも関係します。
agent-framework-foundry
FoundryAgentやWorkflowBuilderを使った複数エージェント構成では、あるエージェントの応答が別のエージェントへ渡されます。
この境界でreasoning項目だけが失われると、次のエージェントが過去のtool callを再生した際にHTTP 400になる可能性があります。1.12.1では、単一エージェントだけでなく、エージェント間の履歴転送を含む回帰テストが追加されています。(GitHub)
agent-framework-foundry-hosting
Foundry Hostingでは、Responses形式の入力をAgent FrameworkのMessageへ変換し、実行結果を再びResponses形式へ戻します。
1.12.1相当の実装では、reasoning項目を変換するときにIDと暗号化データをContentへ保存し、出力時にもencrypted_contentを含むreasoning項目として復元します。
「1.12.1」は全パッケージ共通のバージョンではない
注意したいのは、python-1.12.1がリリース全体のタグであり、すべてのサブパッケージが1.12.1になっているわけではない点です。
リリース時点の主なバージョンは次のとおりです。
| 配布パッケージ | 修正を含むリリース時点のバージョン |
|---|---|
agent-framework | 1.12.1 |
agent-framework-core | 1.12.1 |
agent-framework-openai | 1.11.0 |
agent-framework-foundry | 1.10.3 |
agent-framework-foundry-hosting | 1.0.0b260722 |
たとえば、次の指定は正しくありません。
python -m pip install agent-framework-openai==1.12.1
agent-framework-openaiには独自のバージョン番号が設定されています。個別パッケージを固定している環境では、リリースタグだけを見て全パッケージへ同じ番号を指定しないようにしてください。
Agent Frameworkを1.12.1へ更新する手順
現在のバージョンを確認する
まず、仮想環境を有効にした状態で関連パッケージを確認します。
python -m pip show agent-framework agent-framework-core agent-framework-openai agent-framework-foundry agent-framework-foundry-hosting
インストール済みパッケージを一覧で確認する場合は、次のコマンドも利用できます。
python -m pip freeze
確認すべきポイントは、agent-framework-coreだけが新しく、agent-framework-openaiやagent-framework-foundryが古い状態になっていないかです。
今回の修正は複数パッケージにまたがるため、部分的な更新では問題が残る可能性があります。
メタパッケージを利用している場合
agent-frameworkをまとめて導入している環境では、最小限の変更に抑えるなら1.12.1へ固定します。
python -m pip install --upgrade --upgrade-strategy eager "agent-framework==1.12.1"
--upgrade-strategy eagerを付けるのは、すでにインストールされている古い統合パッケージを、依存条件を満たすという理由だけで残さないためです。
更新後は依存関係を確認します。
python -m pip check
必要なパッケージだけを導入している場合
OpenAIとFoundryを個別に導入している環境では、実際に使用しているパッケージを明示的に更新します。
python -m pip install --upgrade "agent-framework-core==1.12.1" "agent-framework-openai==1.11.0" "agent-framework-foundry==1.10.3"
Foundry Hostingを使用している場合は、ホスティングパッケージも追加します。
python -m pip install --upgrade "agent-framework-foundry-hosting==1.0.0b260722"
Foundry Hostingはこの時点でベータ版です。使用していない環境へ無理に追加する必要はありません。
1.13.0以降へ上げる場合
2026年8月1日時点では、Python向けの新しい安定版として1.13.0も公開されています。1.13.0には1.12.1の修正が含まれますが、ワークフローチェックポイントの再生方法などに破壊的変更が含まれています。
問題修正だけを取り込みたい場合は1.12.1へ固定し、別途1.13.0への移行テストを行う方法が安全です。新規環境で現在の安定版を使う場合も、チェックポイント、承認処理、複数エージェントの回帰テストを実施してください。(GitHub)
更新前のセッションとチェックポイントに注意する
パッケージを更新しても、更新前に保存した会話履歴にreasoning IDや暗号化推論データが含まれていなければ、その履歴を完全に復元できない可能性があります。
これは特に次の構成で注意が必要です。
- 会話履歴をデータベースへ保存している
- ワークフローチェックポイントを永続化している
- 複数のコンテナやワーカー間でセッションを共有している
- バージョン更新後も既存セッションを継続する
- 独自シリアライザーで
ContentをJSON化している
1.12.1の検証処理から判断すると、必要なprotected_dataがない履歴は安全に再生できません。更新後もエラーが続く場合は、新規セッションで再現するかを最初に確認してください。新規セッションでは直る場合、コードではなく保存済み履歴の互換性が原因と考えられます。
修正を確認するスモークテスト
次のコードは、ステートレス設定で1ターン目に関数を呼び出し、2ターン目でその履歴を再利用する最小構成です。
import asyncio
import os
from agent_framework import Agent, AgentSession
from agent_framework.openai import OpenAIChatClient
def lookup_order(order_id: str) -> str:
"""注文状況を取得するテスト用ツール。"""
return f"{order_id}: 発送済み"
async def main() -> None:
model = os.environ["OPENAI_MODEL"]
agent = Agent(
client=OpenAIChatClient(model=model),
instructions=(
"注文状況について回答するときは、"
"必ずlookup_orderツールを呼び出してください。"
),
tools=[lookup_order],
default_options={
"store": False,
"reasoning": {
"effort": "low",
"summary": "auto",
},
},
)
session = AgentSession()
first = await agent.run(
"注文番号A-100の状況を確認してください。",
session=session,
)
print(first)
second = await agent.run(
"先ほど確認した内容を一文で説明してください。",
session=session,
)
print(second)
if __name__ == "__main__":
asyncio.run(main())
このテストでは、次の点を確認します。
- 1ターン目で実際にtool callが発生する
store=Falseのまま2ターン目へ進める- 2ターン目でHTTP 400にならない
- 同じツールが意図せず再実行されない
- ストリーミングと非ストリーミングの両方で成功する
推論モデルは毎回同じ形式を返すとは限りません。1回成功しただけで判断せず、複数回実行してください。実際のアプリケーションが並列tool callを使う場合は、複数のツールを同時に呼ぶケースもテスト対象に含めます。(PyPI)
Foundry環境で実施すべき回帰テスト
単一エージェントのテストだけでは、Foundryのワークフローで発生する問題を十分に確認できません。
| テスト対象 | 確認する内容 |
|---|---|
| 単一エージェント・複数ターン | 1ターン目のtool callを2ターン目で正常に再生できる |
| 複数エージェント | 前段エージェントのreasoningとtool callが後段へ渡る |
WorkflowBuilder | エージェント境界を越えてもHTTP 400にならない |
| クライアント側関数 | function_callとfunction_call_outputの対応が維持される |
| Hosted MCP | mcp_callとreasoningの対応が維持される |
| 並列tool call | 複数のcallとresultが一つのグループとして保持される |
| ミドルウェアによる中断 | 承認拒否やポリシー中断後も履歴が破損しない |
| チェックポイント復元 | JSON化・保存・復元後もreasoning IDとprotected dataが残る |
| Foundry Hosting | ホストのResponses入出力でも暗号化推論データが保持される |
今回の修正では、WorkflowBuilder、FoundryAgent、AgentExecutor、Responses SDKのシリアライズ、Foundry Hostingを通した回帰テストが追加されています。(GitHub)
更新後もエラーになる場合の切り分け
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| reasoningが必要というHTTP 400 | 関連パッケージの一部が古い | pip showでcore・OpenAI・Foundryを確認する |
| 新規セッションでは成功する | 旧セッションに暗号化推論データがない | 既存セッションを終了し、新しいセッションへ切り替える |
Stateless replay cannot reconstructが出る | reasoning IDまたはprotected_dataが欠落している | 履歴の保存形式とコンパクション処理を確認する |
| Duplicate item IDが出る | サービス側継続と履歴の全量再送を併用している | ステートフルかステートレスのどちらかへ統一する |
| 単一エージェントは成功するがワークフローで失敗する | エージェント境界のフィルターがreasoningだけ削除している | context_filterや独自メッセージ変換を確認する |
| Hosted MCPだけ失敗する | Foundry Hostingパッケージが古い | 1.0.0b260722以降へ更新する |
| ときどきしか再現しない | モデルが毎回reasoningとtool callを対で生成するとは限らない | tool callを強制するテストを複数回実施する |
特に独自の履歴フィルターでは、text_reasoningだけを削除しないことが重要です。削除が必要な場合は、関連するtool callとtool resultを含むグループ全体を原子的に扱わなければなりません。
reasoning.encrypted_contentをログへ出さない
reasoning.encrypted_contentやContent.protected_dataは、ステートレス継続に使う不透明なデータです。
暗号化されているからといって、アプリケーションログや監視サービスへ無条件に出力するべきではありません。デバッグ時も次の情報だけを記録する方法が安全です。
- reasoning IDの有無
protected_dataの有無- tool call ID
- tool result ID
- セッションID
- HTTPステータス
- リクエスト相関ID
暗号化データそのものではなく、「存在しているか」「どのIDに対応しているか」を確認してください。
まず関連パッケージの整合と新規セッションで確認する
Agent Frameworkのステートレス実行で推論付きtool callを再生できない問題は、Microsoft Agent Framework for Pythonリリース1.12.1で修正されています。
対処では、agent-framework-coreだけを更新するのではなく、実際に利用しているagent-framework-openai、agent-framework-foundry、agent-framework-foundry-hostingも含めてバージョンを確認してください。
更新後は、最初に新規セッションで2ターン以上のtool callを実行します。新規セッションでは成功し、既存セッションだけが失敗する場合は、更新前に保存されたreasoning IDや暗号化推論データの欠落を疑います。そのうえで、複数エージェント、Hosted MCP、並列tool call、チェックポイント復元までテストするのが確実です。

コメント