ブログ • Open Responses

Open Responses が AAIF に入ったあと、ツール呼び出しを別モデルへ再送する前に JSON の type、arguments、status を何から見るか?

2026年10月22日、Agentic AI Foundation は Open Responses をホストプロジェクトにした。仕様は 1 ターンを item にまとめる。message、function_call、function_call_output が移植可能な中核で、type にプロバイダ接頭辞がある記録はその実装専用。

ログの item を別モデルへ送る前に、ローカルで 3 欄を見る。type にコロンがあれば汎用 function_call にしない。仕様例の arguments は JSON 文字列なので、先に parse しツールの JSON Schema と照合する。status が completed でなければ function_call_output を足さない。

昨日の Bedrock 構造化出力 はモデル本文の形を扱う。Chat Completions の arguments も文字列であることが多いが封筒が違う。AI Agent と JSON Schema。schema 通過は実行許可ではない。権限決定。本記事は Open Responses の item だけを書く。

先にこれだけ:AAIF が受け取ったのは共有 item の形で、1 本の JSON が単一プロバイダ専用のまま残ることがある。以下は type、arguments、status の順で、再送前にどの欄が止めるかを見る。

10月22日、共有 item がオープンガバナンスへ

当日の AGNTCon と MCPCon で、AAIF は 4 プロジェクトを発表した。OpenAI が拠出した Open Responses は MCP、A2A と並び、Linux Foundation 配下のオープンガバナンスに入る。発表は AAIF の発表。同日に Anthropic の Agent Skills、Hugging Face の OpenEnv、アリババの OpenSandbox も入った。本記事はそれらの目録やスキル形式は書かず、Open Responses の item JSON だけを扱う。

同日の AWS オープンソース記事は、Amazon Bedrock が Responses API をサポートし仕様に参加すると書いた。参照は AWS オープンソース記事。フィールドの意味は Open Responses 仕様 に従う。非ストリーミングは application/json、item は type で区別する。ログに function_call があるとき、プロバイダを換えるか翌日再送する前に、どの 3 フィールドが次リクエストを止めるか。

再送できる function_call に最低限どのフィールドがあるか?

次の検査に進める function_call は、少なくとも type、id、name、call_id、status、arguments を持つ。arguments の値は文字列で、その中にツール引数がある。下の例は region が apac で、同じ schema でもう一度検証する。

次の検査に進める function_call。arguments は文字列。
{
  "type": "function_call",
  "id": "fc_001",
  "name": "get_latest_sales_report",
  "call_id": "call_123",
  "status": "completed",
  "arguments": "{\"region\":\"apac\"}"
}

ツールを手元で実行したあと、次リクエストに足すのは function_call_output であり、Chat Completions の tool_calls 配列ではない。仕様の継続例は同じ call_id と output を持つ。サーバが結果を item として返すときに id と status が付く。

次リクエストの function_call_output。call_id は前と同じ。
{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "sales report for apac"
}

type にコロンがあるとき、なぜプロバイダを換えて実行できないか?

仕様は実装が独自 item を足すことを許すが、type にはプロバイダ slug の接頭辞が要る。例は openai:web_search_call。この記録は元のセッションに残し、同じプロバイダへ戻せる。別の Open Responses サーバで移植できるとはみなさない。ローカル規則:コロンがあれば namespaced とし、function_call_output を作らない。

プロバイダ接頭辞付き item。get_latest_sales_report として再実行しない。
{
  "type": "openai:web_search_call",
  "id": "ws_001",
  "status": "completed",
  "action": { "type": "search", "query": "apac sales report" }
}
見ている type別プロバイダへ再送できるかローカルの動作
function_call次の検査へ進める次に arguments と status を見る。
messageinput に置ける会話テキストとして扱い、ツール結果にしない。
openai:web_search_call移植しない元のプロバイダに残すか、横断ログから外す。

なぜ arguments は先に文字列で、そのあと schema なのか?

ストリームの response.function_call_arguments.delta はオブジェクト完成前にテキストを足す。だから保存時の arguments は文字列でありオブジェクトではない。オブジェクトを直接書いたログは次の parse で失敗するか、別実装に拒否される。まず string であることを確認し、json.loads のあとツールの JSON Schema にかける。下の schema は apac、emea、amer だけを許す。

ツール引数 schema。region は apac、emea、amer のみ。
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["region"],
  "properties": {
    "region": {
      "type": "string",
      "enum": ["apac", "emea", "amer"]
    }
  }
}

下の文字列は parse できるが、region が global で enum にない。ゲートは arguments_schema を返し、ツールは実行しない。

parse は成功、schema は失敗。合法な地域を推測しない。
{
  "type": "function_call",
  "id": "fc_009",
  "name": "get_latest_sales_report",
  "call_id": "call_129",
  "status": "completed",
  "arguments": "{\"region\":\"global\"}"
}
見ている argumentsゲート結果ローカルの動作
中身の region が apac の文字列okツールを実行し、function_call_output を書く。
arguments 自体がオブジェクトarguments_type拒否。文字列に戻してから parse。
parse できるが region が globalarguments_schema実行しない。検証エラーを返し、地域を推測しない。

status が completed でないとき、なぜツール結果を書かないか?

仕様は item を状態機械にする。in_progress はまだ出力中。incomplete は終端状態で、その応答の最後の item でなければならず、response 自体も incomplete。completed 以降はその item を変えない。incomplete では arguments が途中で切れていることがあり、半分の JSON は実行しない。

statusfunction_call_output を書いてよいか
in_progressいいえ。ストリームが終わるまで待つ。
incompleteいいえ。終端状態で、引数が切れていることがある。
completedはい。そのあと type にコロンがないこと、arguments が schema を通ること、call_id が次と一致することを確認する。
その他(failed を含む)いいえ。副作用を実行しない。

下のゲートは本稿のサンプルを検査するコードと同じだ。コロン、function_call 以外、status、arguments の型、parse、schema の順で理由を返す。ok のときだけツールを実行し function_call_output を足す。

再送前のローカルゲート。ok 以外はツールを実行しない。
def actual_type(data: object) -> str:
    if data is None:
        return "null"
    if isinstance(data, bool):
        return "boolean"
    if isinstance(data, list):
        return "array"
    if isinstance(data, int):
        return "integer"
    if isinstance(data, float):
        return "number"
    if isinstance(data, str):
        return "string"
    if isinstance(data, dict):
        return "object"
    raise TypeError(type(data))

def validate(data: object, schema: dict) -> list[str]:
    errors: list[str] = []
    if not isinstance(schema, dict):
        return errors
    types = schema.get("type")
    if isinstance(types, str):
        types = [types]
    actual = actual_type(data)
    if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
        errors.append("type")
    if "enum" in schema and not any(value == data for value in schema["enum"]):
        errors.append("enum")
    if isinstance(data, dict) and schema.get("properties"):
        for key in schema.get("required") or []:
            if key not in data:
                errors.append(f"required:{key}")
        if schema.get("additionalProperties") is False:
            for key in data:
                if key not in schema["properties"]:
                    errors.append(f"additional:{key}")
        for key, sub in schema["properties"].items():
            if key in data:
                errors.extend(validate(data[key], sub))
    if isinstance(data, list) and schema.get("items"):
        for item in data:
            errors.extend(validate(item, schema["items"]))
    return errors

def replay_gate(item: dict, schema: dict) -> str:
    item_type = item.get("type")
    if not isinstance(item_type, str) or ":" in item_type:
        return "namespaced"
    if item_type != "function_call":
        return "not_function_call"
    if item.get("status") != "completed":
        return "status"
    arguments = item.get("arguments")
    if not isinstance(arguments, str):
        return "arguments_type"
    try:
        parsed = json.loads(arguments)
    except json.JSONDecodeError:
        return "arguments_parse"
    if validate(parsed, schema):
        return "arguments_schema"
    return "ok"

JSONNote でこの 3 欄をどう分けて見るか?

  1. 1
    arguments が文字列か先に見る

    JSON フォーマット。item 全体を貼る。arguments が 1 本の文字列でなくオブジェクトに広がっていれば、保存形が既にずれており、ゲートは arguments_type を返す。

  2. 2
    次に展開した引数を検証

    arguments 文字列を解いたオブジェクトを JSON Schema に貼る。schema は上の region enum。global は印が付く。実行前に apac へ書き換えない。

  3. 3
    最後に両プロバイダの item を比較

    JSON Diff。左に元プロバイダの item、右に再送予定の item。type が function_call からコロン付き拡張に変わるか、call_id が一致しなければ次リクエストを送らない。

よくある質問

Open Responses が AAIF に入れば、同じ JSON をそのまま Bedrock や別実装へ送れるか?

できない。移植できるのは message、function_call、function_call_output という中核。接頭辞付き item が別サーバで再送できるとはみなさない。Bedrock 上の Claude 構造化出力は別層で、モデル本文の JSON Schema であり、この item 封筒ではない。

arguments をなぜオブジェクトで保存しないか?

仕様例は arguments を JSON 文字列として書く。ストリームでは function_call_arguments.delta で追記し、status が completed になる前の文字列は未完成のことがある。早く parse して実行しない。

status が incomplete のとき、先にツールを実行してよいか?

しない。incomplete は終端状態だ。仕様はその応答の最後の item であること、response 自体も incomplete であることを求める。途中の arguments は schema を通らないことが多い。たまたま通っても完全な呼び出しとして実行しない。

allowed_tools がツール名を限っているのに、ローカル schema はまだ要るか?

要る。allowed_tools が限るのは、このリクエストで実際に実行できるツール名で、ツール定義は tools に全部置ける。arguments 内の region が enum にあるかは見ない。名前が許されても parse と schema は残る。

Chat Completions の tool_calls と Open Responses item を同じ配列に置けるか?

置かない。Open Responses は type、call_id、文字列 arguments を使う。Chat Completions は tool_calls 配列を使う。1 本のログに混ぜると、再送時に別の封筒が次のプロバイダへ行く。Diff で 2 文書を分けて比べる。

まとめ

Open Responses が AAIF に入ったあと、プロバイダ間で共有できるのは item の形であり、すべてのログ行ではない。

結論:type にコロンがあれば汎用 function_call にしない。arguments は文字列であることを確認してからツール schema で検証する。status が completed でなければ function_call_output を書かない。

次:フォーマットで arguments が文字列か見て、Schema で展開後の引数を検証し、Diff で両 item を比べる。

ブログへ戻る